Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/binance-orderbook-ui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ permissions:
jobs:
ui:
runs-on: ubuntu-latest
timeout-minutes: 10
# Include setup and report upload around the complete browser suite.
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
Expand Down
48 changes: 40 additions & 8 deletions docs/binance-order-notifications.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,44 @@ The public factories captured on 2026-10-07 show two independent paths:
cancellation, partial-fill and fill events into toasts through a 30 ms
debounce, without checking the page symbol.

`order-notifications/native-modules.js` pins complete source for `39116`, `55401`
and all six observed `30877` variants. Changes are limited to those notification
`order-notifications/native-modules.js` contains three verified structural
templates for `39116`, `55401` and `30877`. Changes are limited to those notification
consumers. Ordinary events are checked before debounce and immediately before
presentation. Queued tokens contain only a symbol, or null for native handling;
order IDs, client order IDs, prices and quantities are not retained in tokens.
Both audio calls check the same selected token. The sound queue is pruned only
when native playback can start, never while its current cooldown still owns the
queue head.

The `39116` factories in `3314.51597d7f.js` and `3314.86064918.js` have the
same syntax tree apart from consistently renamed local identifiers. Both were
observed in concurrently open tabs on 2026-10-07: the latter left OUSDT and
encoded 龙虾USDT on native sound while USUSDT used the earlier matched source.
AINUSDT also had active sound filtering, confirming that the failure followed
the loaded source rather than the asset name. Runtime matching now uses lexical
binding equivalence rather than source text or a Binance bundle version.
Fixtures identify these variants by source SHA-256 because their module and
chunk IDs are identical. The manifest also associates replacements by source
hash, and browser fixtures require exactly one selected source per module ID.

Acorn parses each targeted factory and eslint-scope resolves its declarations
and references. The structural signature normalizes those local binding names
and removes source positions and ordinary literal formatting. It preserves
global names, object and member keys, import IDs, constants, operators, regular
expressions, template raw values, strict directives and binding relationships.
Shadowing a global or changing an event field therefore changes the signature.
The pure numeric boolean spellings `!0`/`!1` normalize to `true`/`false` because
repackers expand them while formatting. Other truthiness expressions, calls and
side effects remain structurally exact.
An equivalent factory selects a precompiled replacement; host source is never
executed or dynamically compiled by the matcher. The nine captured notification
sources reduce to three structures, including both sound producer variants.

Parsing happens only when a targeted module is captured, not on each order.
Invalid syntax, dynamic scope and sources over 64 KiB are rejected by the
existing capture-failure boundary. This accepts formatting and local renaming;
it does not attempt arbitrary program equivalence or guess changed module IDs.

The page symbol is read on each check, including after a debounce or pending
audio Promise. Each matching tab may notify, even if two tabs show the same
symbol. A symbol with no matching tab produces no ordinary notification in the
Expand Down Expand Up @@ -57,13 +86,14 @@ and failure outcomes; stopping mirror writes does not stop notification scope.
No second queue accessor, global Audio override, WebSocket replacement, new
account subscription or DOM notification removal is installed by this feature.

Both sound factories must execute their pinned replacements before either
changes the native string queue to classified tokens. If either source mismatches,
Both sound factories must execute their verified replacements before either
changes the native string queue to classified tokens. If either structure mismatches,
both sides retain the original string contract. Pending native strings are also
preserved during activation. Toast capture is independent. Late injection,
unknown source or the original 30-second capture deadline leaves the affected
unknown behavior or the original 30-second capture deadline leaves the affected
path native and reports its reason; native reminders may therefore still repeat
after an upstream change.
after a substantive upstream change. Local variable renaming alone does not
require a new userscript release.

`self.__BINANCE_ORDER_NOTIFICATIONS__.snapshot()` exposes only capture status and
aggregate scope-check counts. `suppressedChecks` is a check count, not a unique
Expand Down Expand Up @@ -91,5 +121,7 @@ fill on a BTC page produced the native toast and load/play/play calls. Isolated
tests run the captured adapters, event cache, listeners, React 18.2 hooks and
native debounce with controlled clocks. Only preferences, account stream and
presentation I/O are replaced. Real orders are never required to validate this
feature. Installed-source and live notification behavior remain separate from
these deterministic checks.
feature. An independent esbuild repack also renames and reformats all eight
browser-host modules before exercising foreign-symbol silence and matching
sound through the actual generated userscript. Installed-source and live
notification behavior remain separate from these deterministic checks.
2 changes: 1 addition & 1 deletion docs/binance-orderbook-trade-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ scripts/binance-orderbook-trade.user.js

`order-notifications/` scopes native ordinary order toasts and sound to the
current futures symbol. The storage installer shares its single early queue
observer with these independently pinned targets; no separate userscript is
observer with these independently verified structural targets; no separate userscript is
installed. Risk and unknown events retain native handling. The native account
data flow and trading feedback readers remain unchanged. See
[notification scope](binance-order-notifications.md) for source pins, delayed
Expand Down
93 changes: 93 additions & 0 deletions docs/licenses/binance-notification-parser.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
Acorn
MIT License

Copyright (C) 2012-2022 by various contributors (see AUTHORS)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

eslint-scope
Copyright JS Foundation and other contributors, https://js.foundation
Copyright (C) 2012-2013 Yusuke Suzuki (twitter: @Constellation) and other contributors.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

estraverse
Copyright (C) 2012-2013 Yusuke Suzuki <utatane.tea@gmail.com>
Copyright (C) 2012 Ariya Hidayat <ariya.hidayat@gmail.com>

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

esrecurse
Copyright (C) 2014 Yusuke Suzuki <utatane.tea@gmail.com>

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
47 changes: 41 additions & 6 deletions e2e/binance-orderbook/helpers/order-notifications-host.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { readFile } from 'node:fs/promises';
import { build } from 'esbuild';
import assert from 'node:assert/strict';
import { parse } from 'acorn';
import { build, transform } from 'esbuild';

import { nativeOrderNotificationSources } from '../../../test/fixtures/binance-order-notifications/original-factories.js';
import { openUserscriptScenario } from './userscript-page.js';
import { createCancelScenario } from '../scenarios/cancel-current-symbol.js';

let dependenciesSource;

/** Bundle only library dependencies; captured native factories retain their exact source. */
/** Bundle React dependencies separately from the native factories under test. */
async function readReactDependencies() {
if (!dependenciesSource) {
const result = await build({
Expand Down Expand Up @@ -166,7 +168,10 @@ function installNotificationHost(nativeFactories) {
}

/** Install the built artifact at document-start, then enter through the real Rspack queue. */
export async function openNotificationScenario(page, symbol) {
export async function openNotificationScenario(page, symbol, {
soundSourceSha256 = '5362a54e61f022714e673e166b62e3c22997084ca7a9f4e676475ec91cfcfaa8',
repackNativeFactories = false,
} = {}) {
await page.route('**/*', (route) => route.abort('blockedbyclient'));
const [artifact, runtime, react] = await Promise.all([
readFile(new URL('../../../scripts/binance-orderbook-trade.user.js', import.meta.url), 'utf8'),
Expand All @@ -175,10 +180,40 @@ export async function openNotificationScenario(page, symbol) {
]);
await page.addInitScript({ content: `self.__NOTIFICATION_EARLY_ROOT__=Boolean(document.documentElement);\n${artifact}` });
const ids = new Set(['30877', '39116', '55401', '40477', '70020', '22584', '34122', '4189']);
const factories = nativeOrderNotificationSources.filter((entry) => ids.has(entry.id)
&& (entry.id !== '30877' || entry.chunks.includes('37511'))).map((entry) => entry.source).join(',\n');
const selectedSources = nativeOrderNotificationSources.filter((entry) => ids.has(entry.id)
&& (entry.id !== '30877' || entry.chunks.includes('37511'))
&& (entry.id !== '39116' || entry.sha256 === soundSourceSha256));
const factories = Array.from(ids, (id) => {
const matches = selectedSources.filter((entry) => entry.id === id);
assert.equal(matches.length, 1, `Notification host requires exactly one native factory for ${id}`);
return matches[0].source;
}).join(',\n');
let factoryObject = `{${factories}}`;
if (repackNativeFactories) {
// An independent compiler supplies unseen bindings without using the production matcher.
const capturedFactories = parse(`(${factoryObject});`, { ecmaVersion: 'latest' }).body[0].expression;
const { code } = await transform(`(${factoryObject});`, {
minifyIdentifiers: true,
minifySyntax: false,
minifyWhitespace: false,
});
const program = parse(code, { ecmaVersion: 'latest' });
assert.equal(program.body.length, 1, 'Repacked factories must remain one expression');
const expression = program.body[0].expression;
assert.equal(expression.type, 'ObjectExpression', 'Repacked factories must remain an object');
assert.deepEqual(expression.properties.map((property) => String(property.key.value)), [...ids]);
for (const [index, property] of expression.properties.entries()) {
const captured = selectedSources.find((entry) => entry.id === String(property.key.value));
assert.notEqual(code.slice(property.start, property.end), captured.source,
`Repacking must change the captured source for ${captured.id}`);
assert.notDeepEqual(property.value.params.map((parameter) => parameter.name),
capturedFactories.properties[index].value.params.map((parameter) => parameter.name),
`Repacking must rename factory parameters for ${captured.id}`);
}
factoryObject = code.slice(expression.start, expression.end);
}
const host = await openUserscriptScenario(page, createCancelScenario({ currentSymbol: symbol }), {
beforeOrderbook: `${react}\n${runtime}\n(${installNotificationHost.toString()})({${factories}});\nif(false){`,
beforeOrderbook: `${react}\n${runtime}\n(${installNotificationHost.toString()})(${factoryObject});\nif(false){`,
afterOrderbook: '}',
});
await page.waitForFunction(() => self.__NOTIFICATION_HOST__?.ready() === true);
Expand Down
Loading
Loading