From 4afcbee19b6afe8f00ea2b40c6d6d0d2c3e8c176 Mon Sep 17 00:00:00 2001 From: Tori Date: Thu, 9 Apr 2026 11:19:33 -0500 Subject: [PATCH 1/3] feat: enforce min/max sats amount on orders (#406) Add configurable MAX_PAYMENT_AMT environment variable alongside the existing MIN_PAYMENT_AMT to enforce sats amount limits on orders. Changes: - Add MAX_PAYMENT_AMT to .env-sample (default: 10000000) - Add max validation to validateSellOrder and validateBuyOrder - Add min/max validation to the interactive wizard (scenes.ts) - Add mustBeLessEqThan message helper in messages.ts - Add must_be_lt_or_eq locale key in all 10 languages - Add tests for max exceeded, equal to max, boundary, and market price (amount=0) edge cases The max check is optional: if MAX_PAYMENT_AMT is not set, no upper limit is enforced (backwards compatible). Amount 0 (market price orders) always bypasses both min and max checks. Closes #406 --- .env-sample | 2 + bot/messages.ts | 18 +++++ bot/modules/orders/scenes.ts | 18 +++++ bot/validations.ts | 13 +++- locales/de.yaml | 1 + locales/en.yaml | 1 + locales/es.yaml | 1 + locales/fa.yaml | 1 + locales/fr.yaml | 1 + locales/it.yaml | 1 + locales/ko.yaml | 1 + locales/pt.yaml | 1 + locales/ru.yaml | 1 + locales/uk.yaml | 1 + tests/bot/validation.spec.ts | 124 +++++++++++++++++++++++++++++++++++ 15 files changed, 184 insertions(+), 1 deletion(-) diff --git a/.env-sample b/.env-sample index 5351191c..99940b0d 100644 --- a/.env-sample +++ b/.env-sample @@ -51,6 +51,8 @@ ORDER_PUBLISHED_EXPIRATION_WINDOW=82800 # Minimum amount for a payment in satoshis MIN_PAYMENT_AMT=1 +# Maximum amount for a payment in satoshis +MAX_PAYMENT_AMT=10000000 # Maximum number of orders that a user can have published (PENDING) at the same time MAX_PENDING_ORDERS=4 diff --git a/bot/messages.ts b/bot/messages.ts index 00011d13..93bee4da 100644 --- a/bot/messages.ts +++ b/bot/messages.ts @@ -945,6 +945,23 @@ const mustBeGreatherEqThan = async ( } }; +const mustBeLessEqThan = async ( + ctx: MainContext, + fieldName: string, + qty: number, +) => { + try { + await ctx.reply( + ctx.i18n.t('must_be_lt_or_eq', { + fieldName, + qty, + }), + ); + } catch (error) { + logger.error(error); + } +}; + const bannedUserErrorMessage = async (ctx: MainContext, user: UserDocument) => { try { await ctx.telegram.sendMessage( @@ -2149,6 +2166,7 @@ export { termsMessage, privacyMessage, mustBeGreatherEqThan, + mustBeLessEqThan, bannedUserErrorMessage, fiatSentMessages, orderOnfiatSentStatusMessages, diff --git a/bot/modules/orders/scenes.ts b/bot/modules/orders/scenes.ts index e7db533e..d9f22032 100644 --- a/bot/modules/orders/scenes.ts +++ b/bot/modules/orders/scenes.ts @@ -316,6 +316,24 @@ const createOrderHandlers = { await ctx.wizard.state.updateUI(); return; } + const minPaymentAmt = Number(process.env.MIN_PAYMENT_AMT) || 0; + const maxPaymentAmt = Number(process.env.MAX_PAYMENT_AMT) || 0; + if (input !== 0 && minPaymentAmt > 0 && input < minPaymentAmt) { + ctx.wizard.state.error = ctx.i18n.t('must_be_gt_or_eq', { + fieldName: ctx.i18n.t('sats_amount'), + qty: minPaymentAmt, + }); + await ctx.wizard.state.updateUI(); + return; + } + if (input !== 0 && maxPaymentAmt > 0 && input > maxPaymentAmt) { + ctx.wizard.state.error = ctx.i18n.t('must_be_lt_or_eq', { + fieldName: ctx.i18n.t('sats_amount'), + qty: maxPaymentAmt, + }); + await ctx.wizard.state.updateUI(); + return; + } ctx.wizard.state.sats = Math.floor(input); await ctx.wizard.state.updateUI(); return true; diff --git a/bot/validations.ts b/bot/validations.ts index 7ee42bbb..33bf211a 100644 --- a/bot/validations.ts +++ b/bot/validations.ts @@ -203,7 +203,6 @@ const validateSellOrder = async (ctx: MainContext) => { return false; } - // TODO, this validation could be amount > 0? if (amount !== 0 && amount < Number(process.env.MIN_PAYMENT_AMT)) { await messages.mustBeGreatherEqThan( ctx, @@ -213,6 +212,12 @@ const validateSellOrder = async (ctx: MainContext) => { return false; } + const maxPaymentAmt = Number(process.env.MAX_PAYMENT_AMT) || 0; + if (amount !== 0 && maxPaymentAmt > 0 && amount > maxPaymentAmt) { + await messages.mustBeLessEqThan(ctx, 'monto_en_sats', maxPaymentAmt); + return false; + } + if (fiatAmount.length === 2 && fiatAmount[1] <= fiatAmount[0]) { await messages.mustBeANumberOrRange(ctx); return false; @@ -301,6 +306,12 @@ const validateBuyOrder = async (ctx: MainContext) => { return false; } + const maxPaymentAmt = Number(process.env.MAX_PAYMENT_AMT) || 0; + if (amount !== 0 && maxPaymentAmt > 0 && amount > maxPaymentAmt) { + await messages.mustBeLessEqThan(ctx, 'monto_en_sats', maxPaymentAmt); + return false; + } + if (fiatAmount.length === 2 && fiatAmount[1] <= fiatAmount[0]) { await messages.mustBeANumberOrRange(ctx); return false; diff --git a/locales/de.yaml b/locales/de.yaml index 23f92e6d..e115c3ae 100644 --- a/locales/de.yaml +++ b/locales/de.yaml @@ -282,6 +282,7 @@ help: | /version - Zeigt die aktuelle Version des Bots /help - Hilfe must_be_gt_or_eq: ${fieldName} muss ${qty} oder mehr entsprechen +must_be_lt_or_eq: ${fieldName} muss ${qty} oder weniger entsprechen you_have_been_banned: Du wurdest gesperrt! I_told_seller_you_sent_fiat: 🤖 Ich habe @${sellerUsername} gesagt, dass du FIAT-Geld geschickt hast. Wenn der Verkäufer bestätigt, dass er dein Geld erhalten hat, muss er die Mittel freigeben. Falls er sich weigert, kannst du eine Streitigkeit eröffnen. buyer_told_me_that_sent_fiat: | diff --git a/locales/en.yaml b/locales/en.yaml index 117a99c1..31d7fa3c 100644 --- a/locales/en.yaml +++ b/locales/en.yaml @@ -287,6 +287,7 @@ help: | version: Version commit_hash: Hash of last commit must_be_gt_or_eq: ${fieldName} Must be greater or equal to ${qty} +must_be_lt_or_eq: ${fieldName} Must be less or equal to ${qty} you_have_been_banned: You have been banned! I_told_seller_you_sent_fiat: 🤖 I told @${sellerUsername} that you have sent the fiat money. When the seller confirms that they have received your money, they should release the funds. If they refuse, you can open a dispute. buyer_told_me_that_sent_fiat: | diff --git a/locales/es.yaml b/locales/es.yaml index 6d75ce95..5d22c2b5 100644 --- a/locales/es.yaml +++ b/locales/es.yaml @@ -285,6 +285,7 @@ help: | version: Versión commit_hash: Hash del último commit must_be_gt_or_eq: ${fieldName} debe ser mayor o igual que ${qty} +must_be_lt_or_eq: ${fieldName} debe ser menor o igual que ${qty} you_have_been_banned: ¡Has sido baneado! I_told_seller_you_sent_fiat: 🤖 Le avisé a @${sellerUsername} que has enviado el dinero fiat, cuando el vendedor confirme que recibió tu dinero deberá liberar los fondos. Si se niega, puedes abrir una disputa. buyer_told_me_that_sent_fiat: | diff --git a/locales/fa.yaml b/locales/fa.yaml index b1633743..99aee2e3 100644 --- a/locales/fa.yaml +++ b/locales/fa.yaml @@ -366,6 +366,7 @@ help: | version: نسخه commit_hash: هش آخرین پرداخت وثیقه must_be_gt_or_eq: '${fieldName} باید بزرگ‌تر یا برابر با ${qty} باشد.' +must_be_lt_or_eq: '${fieldName} باید کوچک‌تر یا برابر با ${qty} باشد.' you_have_been_banned: 'شما محروم شده‌اید!' I_told_seller_you_sent_fiat: '🤖 من به @${sellerUsername} خبر دادم که شما پول فیات را فرستاده‌اید. فروشنده باید ساتوشی‌ها را پس از بررسی اینکه پول شما را دریافت کرده است، آزاد کند. اگر او این کار را نکرد، می‌توانید یک مشاجره ثبت کنید.' buyer_told_me_that_sent_fiat: | diff --git a/locales/fr.yaml b/locales/fr.yaml index 05e128a2..3e2506f8 100644 --- a/locales/fr.yaml +++ b/locales/fr.yaml @@ -284,6 +284,7 @@ help: | /version - Affiche la version actuelle du bot /help - Messages d'aide must_be_gt_or_eq: ${fieldName} Doit être supérieur ou égal à ${qty} +must_be_lt_or_eq: ${fieldName} Doit être inférieur ou égal à ${qty} you_have_been_banned: Tu as été banni ! I_told_seller_you_sent_fiat: "🤖 J'ai dit à @${sellerUsername} que tu as envoyé le paiement fiat. Lorsque le vendeur confirmera avoir reçu ton argent, il devra libérer les fonds. S'il refuse, tu peux ouvrir un litige." buyer_told_me_that_sent_fiat: | diff --git a/locales/it.yaml b/locales/it.yaml index 7ecb9e5a..784847b7 100644 --- a/locales/it.yaml +++ b/locales/it.yaml @@ -282,6 +282,7 @@ help: | /version - mostra la versione corrente del bot /help - messaggi di aiuto must_be_gt_or_eq: ${fieldName} Deve essere superiore o uguale a ${qty} +must_be_lt_or_eq: ${fieldName} Deve essere inferiore o uguale a ${qty} you_have_been_banned: Sei stato bannato! I_told_seller_you_sent_fiat: 🤖 Ho avvisato @${sellerUsername} che hai inviato il denaro fiat, quando il venditore confermerà di aver ricevuto il tuo denaro dovrà liberare i fondi. Se si rifiuta, puoi aprire una disputa. buyer_told_me_that_sent_fiat: | diff --git a/locales/ko.yaml b/locales/ko.yaml index 76ef36fc..33c40edd 100644 --- a/locales/ko.yaml +++ b/locales/ko.yaml @@ -283,6 +283,7 @@ help: | /version - 봇의 현재 버전을 보여줍니다. /help - 도움말을 보여줍니다. must_be_gt_or_eq: ${fieldName}은 최소 ${qty}보다 크거나 같아야 합니다. +must_be_lt_or_eq: ${fieldName}은 ${qty}보다 작거나 같아야 합니다. you_have_been_banned: 당신은 추방되었습니다! I_told_seller_you_sent_fiat: 🤖 @${sellerUsername} 에게 당신이 fiat를 송금했다고 알렸습니다. 판매자가 돈을 받았다고 확인하면 자금을 해제해야 합니다. 만약 거부하면 분쟁을 열 수 있습니다. buyer_told_me_that_sent_fiat: | diff --git a/locales/pt.yaml b/locales/pt.yaml index 4ef9817a..32861e6d 100644 --- a/locales/pt.yaml +++ b/locales/pt.yaml @@ -283,6 +283,7 @@ help: | /version - mostra a versão atual do bot /help - mensagem de ajuda must_be_gt_or_eq: ${fieldName} Deve ser mais ou igual a ${qty} +must_be_lt_or_eq: ${fieldName} Deve ser menor ou igual a ${qty} you_have_been_banned: Você foi banido! I_told_seller_you_sent_fiat: 🤖 Informei a @${sellerUsername} que você enviou o dinheiro fiat, quando o vendedor confirmar que recebeu seu dinheiro, ele deverá liberar os fundos. Se ele se recusar, você pode abrir uma disputa. buyer_told_me_that_sent_fiat: | diff --git a/locales/ru.yaml b/locales/ru.yaml index 3d759562..b27f1f50 100644 --- a/locales/ru.yaml +++ b/locales/ru.yaml @@ -281,6 +281,7 @@ help: | /version - Показывает текущую версию бота /help - Показать ключевые команды must_be_gt_or_eq: ${fieldName} должно быть больше или равно ${qty} +must_be_lt_or_eq: ${fieldName} должно быть меньше или равно ${qty} you_have_been_banned: Вы были забанены! I_told_seller_you_sent_fiat: 🤖 Я сообщил @${sellerUsername}, что ты отправил фиат, когда продавец подтвердит получение денег, он должен освободить средства. Если он откажется, можешь открыть спор. buyer_told_me_that_sent_fiat: | diff --git a/locales/uk.yaml b/locales/uk.yaml index 80f42571..b362579f 100644 --- a/locales/uk.yaml +++ b/locales/uk.yaml @@ -281,6 +281,7 @@ help: | /version - Показує поточну версію бота /help - Показати ключові команди must_be_gt_or_eq: ${fieldName} має бути більше чи рівно ${qty} +must_be_lt_or_eq: ${fieldName} має бути менше чи рівно ${qty} you_have_been_banned: Ви були забанені! I_told_seller_you_sent_fiat: 🤖 Я повідомив @${sellerUsername}, що ти надіслав фіат, коли продавець підтвердить отримання твоїх грошей, він звільнить кошти. Якщо він відмовиться, ти можеш відкрити спір. buyer_told_me_that_sent_fiat: | diff --git a/tests/bot/validation.spec.ts b/tests/bot/validation.spec.ts index 63a5f3a6..fe3e33f5 100644 --- a/tests/bot/validation.spec.ts +++ b/tests/bot/validation.spec.ts @@ -143,6 +143,71 @@ describe('Validations', () => { expect(replyStub.calledOnce).to.equal(true); }); + it('should return false if amount exceeds maximum', async () => { + sandbox.restore(); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'env').value({ + MIN_PAYMENT_AMT: 100, + MAX_PAYMENT_AMT: 5000, + NODE_ENV: 'production', + INVOICE_EXPIRATION_WINDOW: 3600000, + }); + ctx.state.command.args = ['6000', '100', 'USD', 'zelle']; + const result = await validateSellOrder(ctx); + expect(result).to.equal(false); + expect(replyStub.calledOnce).to.equal(true); + }); + + it('should allow amount equal to maximum', async () => { + sandbox.restore(); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'env').value({ + MIN_PAYMENT_AMT: 100, + MAX_PAYMENT_AMT: 5000, + NODE_ENV: 'production', + INVOICE_EXPIRATION_WINDOW: 3600000, + }); + ctx.state.command.args = ['5000', '100', 'USD', 'zelle']; + const result = await validateSellOrder(ctx); + expect(result).to.be.an('object'); + }); + + it('should skip max check when MAX_PAYMENT_AMT is not set', async () => { + ctx.state.command.args = ['10000', '100', 'USD', 'zelle']; + const result = await validateSellOrder(ctx); + expect(result).to.be.an('object'); + }); + + it('should allow amount 0 (market price) even with max set', async () => { + sandbox.restore(); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'env').value({ + MIN_PAYMENT_AMT: 100, + MAX_PAYMENT_AMT: 5000, + NODE_ENV: 'production', + INVOICE_EXPIRATION_WINDOW: 3600000, + }); + ctx.state.command.args = ['0', '100-200', 'USD', 'zelle']; + const result = await validateSellOrder(ctx); + expect(result).to.be.an('object'); + if (result === false) throw new Error('object expected'); + expect(result.amount).to.equal(0); + }); + + it('should return false if amount is exactly one above maximum', async () => { + sandbox.restore(); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'env').value({ + MIN_PAYMENT_AMT: 100, + MAX_PAYMENT_AMT: 5000, + NODE_ENV: 'production', + INVOICE_EXPIRATION_WINDOW: 3600000, + }); + ctx.state.command.args = ['5001', '100', 'USD', 'zelle']; + const result = await validateSellOrder(ctx); + expect(result).to.equal(false); + }); + it('should return object if validation success', async () => { ctx.state.command.args = ['10000', '100', 'USD', 'zelle']; const result = await validateSellOrder(ctx); @@ -217,6 +282,65 @@ describe('Validations', () => { expect(replyStub.calledOnce).to.be.equal(true); }); + it('should return false if amount exceeds maximum', async () => { + sandbox.restore(); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'env').value({ + MIN_PAYMENT_AMT: 100, + MAX_PAYMENT_AMT: 5000, + NODE_ENV: 'production', + INVOICE_EXPIRATION_WINDOW: 3600000, + }); + ctx.state.command.args = ['6000', '100', 'USD', 'zelle']; + const result = await validateBuyOrder(ctx); + expect(result).to.equal(false); + expect(replyStub.calledOnce).to.equal(true); + }); + + it('should allow amount equal to maximum', async () => { + sandbox.restore(); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'env').value({ + MIN_PAYMENT_AMT: 100, + MAX_PAYMENT_AMT: 5000, + NODE_ENV: 'production', + INVOICE_EXPIRATION_WINDOW: 3600000, + }); + ctx.state.command.args = ['5000', '100', 'USD', 'zelle']; + const result = await validateBuyOrder(ctx); + expect(result).to.be.an('object'); + }); + + it('should allow amount 0 (market price) even with max set', async () => { + sandbox.restore(); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'env').value({ + MIN_PAYMENT_AMT: 100, + MAX_PAYMENT_AMT: 5000, + NODE_ENV: 'production', + INVOICE_EXPIRATION_WINDOW: 3600000, + }); + ctx.state.command.args = ['0', '100-200', 'USD', 'zelle']; + const result = await validateBuyOrder(ctx); + expect(result).to.be.an('object'); + if (result === false) throw new Error('object expected'); + expect(result.amount).to.equal(0); + }); + + it('should return false if amount is exactly one above maximum', async () => { + sandbox.restore(); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'env').value({ + MIN_PAYMENT_AMT: 100, + MAX_PAYMENT_AMT: 5000, + NODE_ENV: 'production', + INVOICE_EXPIRATION_WINDOW: 3600000, + }); + ctx.state.command.args = ['5001', '100', 'USD', 'zelle']; + const result = await validateBuyOrder(ctx); + expect(result).to.equal(false); + }); + it('should return object if validation success', async () => { ctx.state.command.args = ['10000', '100', 'USD', 'zelle']; const result = await validateBuyOrder(ctx); From c57f157c78f12d709de377c13f9b0821d9513822 Mon Sep 17 00:00:00 2001 From: Tori Date: Mon, 4 May 2026 22:34:00 -0500 Subject: [PATCH 2/3] fix: normalize sats input before min/max validation in wizard Ensure Math.floor is applied before comparing against MIN/MAX_PAYMENT_AMT so the value checked matches the value stored in wizard state. Also export createOrderHandlers and add wizard-path tests for min/max bounds. --- bot/modules/orders/scenes.ts | 11 ++-- tests/bot/validation.spec.ts | 101 +++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/bot/modules/orders/scenes.ts b/bot/modules/orders/scenes.ts index d9f22032..91ff6e62 100644 --- a/bot/modules/orders/scenes.ts +++ b/bot/modules/orders/scenes.ts @@ -260,7 +260,7 @@ const createOrderPrompts = { }, }; -const createOrderHandlers = { +export const createOrderHandlers = { async fiatAmount(ctx: CommunityContext) { if (ctx.message === undefined) return ctx.scene.leave(); ctx.wizard.state.error = null; @@ -304,18 +304,19 @@ const createOrderHandlers = { await ctx.wizard.state.updateUI(); return true; } - const input = Number(ctx.message?.text); + const rawInput = Number(ctx.message?.text); await ctx.deleteMessage(); - if (isNaN(input)) { + if (isNaN(rawInput)) { ctx.wizard.state.error = ctx.i18n.t('not_number'); await ctx.wizard.state.updateUI(); return; } - if (input < 0) { + if (rawInput < 0) { ctx.wizard.state.error = ctx.i18n.t('not_negative'); await ctx.wizard.state.updateUI(); return; } + const input = Math.floor(rawInput); const minPaymentAmt = Number(process.env.MIN_PAYMENT_AMT) || 0; const maxPaymentAmt = Number(process.env.MAX_PAYMENT_AMT) || 0; if (input !== 0 && minPaymentAmt > 0 && input < minPaymentAmt) { @@ -334,7 +335,7 @@ const createOrderHandlers = { await ctx.wizard.state.updateUI(); return; } - ctx.wizard.state.sats = Math.floor(input); + ctx.wizard.state.sats = input; await ctx.wizard.state.updateUI(); return true; }, diff --git a/tests/bot/validation.spec.ts b/tests/bot/validation.spec.ts index fe3e33f5..606b2682 100644 --- a/tests/bot/validation.spec.ts +++ b/tests/bot/validation.spec.ts @@ -12,6 +12,7 @@ import { validateUserWaitingOrder, isBannedFromCommunity, } from '../../bot/validations'; +import { createOrderHandlers } from '../../bot/modules/orders/scenes'; import * as messages from '../../bot/messages'; import { Order, User, Community } from '../../models'; import { IOrder } from '../../models/order'; @@ -1225,6 +1226,106 @@ describe('Validations', () => { }); }); + describe('createOrderHandlers.sats (wizard path)', () => { + let wizardCtx: any; + + beforeEach(() => { + wizardCtx = { + callbackQuery: undefined, + message: { text: '1000' }, + i18n: { + t: (key: string, _params?: any) => key, + }, + wizard: { + state: { + sats: undefined, + error: undefined, + updateUI: sinon.stub().resolves(), + }, + }, + deleteMessage: sinon.stub().resolves(), + }; + }); + + it('should set error when amount exceeds maximum', async () => { + sandbox.restore(); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'env').value({ + MIN_PAYMENT_AMT: 100, + MAX_PAYMENT_AMT: 5000, + NODE_ENV: 'test', + }); + wizardCtx.message.text = '6000'; + const result = await createOrderHandlers.sats(wizardCtx); + expect(result).to.equal(undefined); + expect(wizardCtx.wizard.state.error).to.equal('must_be_lt_or_eq'); + expect(wizardCtx.wizard.state.updateUI.calledOnce).to.equal(true); + }); + + it('should allow amount equal to maximum', async () => { + sandbox.restore(); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'env').value({ + MIN_PAYMENT_AMT: 100, + MAX_PAYMENT_AMT: 5000, + NODE_ENV: 'test', + }); + wizardCtx.message.text = '5000'; + const result = await createOrderHandlers.sats(wizardCtx); + expect(result).to.equal(true); + expect(wizardCtx.wizard.state.sats).to.equal(5000); + }); + + it('should skip max check when MAX_PAYMENT_AMT is not set', async () => { + wizardCtx.message.text = '99999'; + const result = await createOrderHandlers.sats(wizardCtx); + expect(result).to.equal(true); + expect(wizardCtx.wizard.state.sats).to.equal(99999); + }); + + it('should allow amount 0 (market price) even with max set', async () => { + sandbox.restore(); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'env').value({ + MIN_PAYMENT_AMT: 100, + MAX_PAYMENT_AMT: 5000, + NODE_ENV: 'test', + }); + wizardCtx.message.text = '0'; + const result = await createOrderHandlers.sats(wizardCtx); + expect(result).to.equal(true); + expect(wizardCtx.wizard.state.sats).to.equal(0); + }); + + it('should reject amount one above maximum', async () => { + sandbox.restore(); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'env').value({ + MIN_PAYMENT_AMT: 100, + MAX_PAYMENT_AMT: 5000, + NODE_ENV: 'test', + }); + wizardCtx.message.text = '5001'; + const result = await createOrderHandlers.sats(wizardCtx); + expect(result).to.equal(undefined); + expect(wizardCtx.wizard.state.error).to.equal('must_be_lt_or_eq'); + }); + + it('should accept decimal that floors to exactly the maximum', async () => { + sandbox.restore(); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'env').value({ + MIN_PAYMENT_AMT: 100, + MAX_PAYMENT_AMT: 5000, + NODE_ENV: 'test', + }); + wizardCtx.message.text = '5000.9'; + const result = await createOrderHandlers.sats(wizardCtx); + expect(result).to.equal(true); + expect(wizardCtx.wizard.state.sats).to.equal(5000); + }); + }); + describe('isBannedFromCommunity', () => { beforeEach(() => { community = { From c2410390cde469ffc2d4f11083f928c7d8642ada Mon Sep 17 00:00:00 2001 From: Tori Date: Mon, 4 May 2026 22:59:51 -0500 Subject: [PATCH 3/3] fix: use Number.isFinite for MAX_PAYMENT_AMT guard in validations Replace the truthy-string check with Number.isFinite so NaN and Infinity are explicitly handled. Also clarify in .env-sample that MAX_PAYMENT_AMT is optional and leaving it unset disables the upper bound. --- .env-sample | 2 +- bot/validations.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.env-sample b/.env-sample index 99940b0d..de6bb174 100644 --- a/.env-sample +++ b/.env-sample @@ -51,7 +51,7 @@ ORDER_PUBLISHED_EXPIRATION_WINDOW=82800 # Minimum amount for a payment in satoshis MIN_PAYMENT_AMT=1 -# Maximum amount for a payment in satoshis +# Maximum amount for a payment in satoshis (optional, in satoshis). Leave unset or set to 0 to disable the upper bound. MAX_PAYMENT_AMT=10000000 # Maximum number of orders that a user can have published (PENDING) at the same time diff --git a/bot/validations.ts b/bot/validations.ts index 33bf211a..62664ff9 100644 --- a/bot/validations.ts +++ b/bot/validations.ts @@ -212,8 +212,8 @@ const validateSellOrder = async (ctx: MainContext) => { return false; } - const maxPaymentAmt = Number(process.env.MAX_PAYMENT_AMT) || 0; - if (amount !== 0 && maxPaymentAmt > 0 && amount > maxPaymentAmt) { + const maxPaymentAmt = Number(process.env.MAX_PAYMENT_AMT); + if (amount !== 0 && Number.isFinite(maxPaymentAmt) && maxPaymentAmt > 0 && amount > maxPaymentAmt) { await messages.mustBeLessEqThan(ctx, 'monto_en_sats', maxPaymentAmt); return false; } @@ -306,8 +306,8 @@ const validateBuyOrder = async (ctx: MainContext) => { return false; } - const maxPaymentAmt = Number(process.env.MAX_PAYMENT_AMT) || 0; - if (amount !== 0 && maxPaymentAmt > 0 && amount > maxPaymentAmt) { + const maxPaymentAmt = Number(process.env.MAX_PAYMENT_AMT); + if (amount !== 0 && Number.isFinite(maxPaymentAmt) && maxPaymentAmt > 0 && amount > maxPaymentAmt) { await messages.mustBeLessEqThan(ctx, 'monto_en_sats', maxPaymentAmt); return false; }