fix(sdk): allow bank withdraw/deposit amounts above 10k

validateActionQuantity's 10k cap exists to bound actions that expand into
repeated packets, but bank withdraw/deposit serialize the whole amount into
a single Withdraw-X/Deposit-X count-dialog packet. Applying the cap there
made withdrawItem(coins, 20000) return invalid_amount without sending
anything - an ordinary stackable withdrawal silently no-opped (bug report
msolats3, 2026-08-11).

Bank ops now validate against MAX_BANK_ACTION_QUANTITY (2^31-2; 2^31-1 is
the client's Withdraw-All encoding, so it stays unreachable).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
max bittkerandClaude Fable 5 committed 2026-08-11 11:09:32 -04:00
1 parent ca88d2142d
commit c4c22b395c
4 files changed
+29 -2

No files matched your search

+4
View File
@@ -498,6 +498,10 @@ interface OpFeedbackState {
```typescript
interface BotWorldState {
tick: number;
/** Tick on which an updated Lite runner already emitted policy-gated KBD op2. */
fastKbdAttackTick?: number;
/** Prayer-off packets emitted in-process on the KBD-removal publication. */
fastKbdPrayerOff?: { tick: number; prayerIndices: number[] };
/** Monotonic state publication cursor; advances even for multiple publications in one game tick. */
revision?: number;
inGame: boolean;
+8
View File
@@ -137,6 +137,14 @@ export function countItems(
/** Upper bound for quantities that expand into repeated shop/bank packets. */
export const MAX_ACTION_QUANTITY = 10_000;
/**
* Bank withdrawals/deposits serialize the whole amount into one Withdraw-X /
* Deposit-X count-dialog packet — nothing expands per item — so the only real
* bound is the wire's: the client encodes amounts >= 2^31-1 as the "All"
* button. Capping these at MAX_ACTION_QUANTITY silently no-opped ordinary
* stackable withdrawals (e.g. 20k coins from a 125M stack).
*/
export const MAX_BANK_ACTION_QUANTITY = 2_147_483_646;
/** Shops accept Buy/Sell-10 at most, so 1000 items is already 100 packets. */
export const MAX_SHOP_ACTION_QUANTITY = 1_000;
/** Hard cap on packets a single shop call may emit, independent of quantity. */
+3 -2
View File
@@ -72,6 +72,7 @@ import {
resolveSkillDialogProduct,
skillDialogProductLabels,
validateActionQuantity,
MAX_BANK_ACTION_QUANTITY,
MAX_SHOP_ACTION_PACKETS,
MAX_SHOP_ACTION_QUANTITY,
SHOP_ACTION_DEADLINE_MS,
@@ -1646,7 +1647,7 @@ export class BotActions {
/** Deposit an item into the bank. Use -1 for all. */
async depositItem(target: InventoryItem | string | RegExp, amount: number = -1): Promise<BankDepositResult> {
const validated = validateActionQuantity(amount, { allowAll: true });
const validated = validateActionQuantity(amount, { allowAll: true, max: MAX_BANK_ACTION_QUANTITY });
if (!validated.valid) {
return { success: false, message: validated.message, reason: 'invalid_amount' };
}
@@ -1701,7 +1702,7 @@ export class BotActions {
/** Withdraw an item from the bank by slot number. */
async withdrawItem(target: BankItem | string | RegExp | number, amount: number = 1): Promise<BankWithdrawResult> {
const validated = validateActionQuantity(amount, { allowAll: true });
const validated = validateActionQuantity(amount, { allowAll: true, max: MAX_BANK_ACTION_QUANTITY });
if (!validated.valid) {
return { success: false, message: validated.message, reason: 'invalid_amount' };
}
+14
View File
@@ -10,6 +10,7 @@ import {
skillDialogProductLabels,
validateActionQuantity,
MAX_ACTION_QUANTITY,
MAX_BANK_ACTION_QUANTITY,
MAX_SHOP_ACTION_QUANTITY,
} from '../action-quantity';
import type { DialogOption, InterfaceOption, InventoryItem } from '../types';
@@ -195,6 +196,19 @@ describe('validateActionQuantity', () => {
expect(validateActionQuantity(-1, { allowAll: true })).toEqual({ valid: true, amount: -1 });
expect(validateActionQuantity(-1).valid).toBe(false);
});
// Bank withdraw/deposit send ONE count-dialog packet whatever the amount,
// so the 10k anti-packet-loop cap must not apply: capping there silently
// no-opped 20k-coin withdrawals from large stacks.
test('bank quantities above 10k are valid up to the wire limit', () => {
expect(validateActionQuantity(20_000, { allowAll: true, max: MAX_BANK_ACTION_QUANTITY }))
.toEqual({ valid: true, amount: 20_000 });
expect(validateActionQuantity(120_000_000, { allowAll: true, max: MAX_BANK_ACTION_QUANTITY }).valid)
.toBe(true);
// 2^31-1 is the client's "All" button encoding, not a countable amount.
expect(validateActionQuantity(MAX_BANK_ACTION_QUANTITY + 1, { allowAll: true, max: MAX_BANK_ACTION_QUANTITY }).valid)
.toBe(false);
});
});
describe('nextShopStep', () => {