From 77c9f20b62439cf38db434250390277641fff027 Mon Sep 17 00:00:00 2001 From: phroi <90913182+phroi@users.noreply.github.com> Date: Wed, 27 May 2026 17:37:52 +0000 Subject: [PATCH 1/3] fix(order): skip non-positive match candidates --- packages/order/src/order.test.ts | 63 ++++++++++++++++++++++++++++++-- packages/order/src/order.ts | 24 +++++++----- 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/packages/order/src/order.test.ts b/packages/order/src/order.test.ts index c71509a8..ca21c4ad 100644 --- a/packages/order/src/order.test.ts +++ b/packages/order/src/order.test.ts @@ -242,6 +242,7 @@ describe("OrderMatcher", () => { udtToCkb: { matchableCount: 1 }, }, candidates: { + bestGain: 0n, positiveGain: 0, }, }, @@ -249,6 +250,55 @@ describe("OrderMatcher", () => { expect(match.diagnostics?.candidates.rejected.nonPositiveGain).toBeGreaterThan(0); }); + it("does not select non-positive candidates after rejecting the empty allowance", () => { + const order = makeOrderCell({ + ckbUnoccupied: ccc.fixedPointFrom(200), + udtValue: 0n, + info: Info.create(true, { ckbScale: 1n, udtScale: 1n }), + master: { + type: "absolute", + value: { + txHash: byte32FromByte("33"), + index: 1n, + }, + }, + outPoint: { + txHash: byte32FromByte("55"), + index: 0n, + }, + }); + + const match = OrderManager.bestMatch( + [order], + { + ckbValue: -1n, + udtValue: ccc.fixedPointFrom(1000), + }, + { + ckbScale: 1n, + udtScale: 1n, + }, + { + feeRate: 0n, + ckbAllowanceStep: ccc.fixedPointFrom(1), + }, + ); + + expect(match).toMatchObject({ + ckbDelta: 0n, + udtDelta: 0n, + partials: [], + diagnostics: { + candidates: { + bestGain: 0n, + positiveGain: 0, + }, + }, + }); + expect(match.diagnostics?.candidates.rejected.insufficientCkbAllowance).toBeGreaterThan(0); + expect(match.diagnostics?.candidates.rejected.nonPositiveGain).toBeGreaterThan(0); + }); + it("reports one primary allowance rejection reason per candidate", () => { const order = makeUdtToCkbOrder(); @@ -1734,8 +1784,8 @@ function exhaustiveSequentialBestMatch( const udtAllowanceStep = ( options.ckbAllowanceStep * exchangeRate.ckbScale + exchangeRate.udtScale - 1n ) / exchangeRate.udtScale; - let best: Match | undefined; - let bestGain = -1n << 256n; + let best: Match = { ckbDelta: 0n, udtDelta: 0n, partials: [] }; + let bestGain = 0n; for (const c2u of OrderManager.sequentialMatcher( orderPool, true, @@ -1763,13 +1813,18 @@ function exhaustiveSequentialBestMatch( const udtAllowance = allowance.udtValue + udtDelta; const gain = (ckbDelta - ckbFee) * exchangeRate.ckbScale + udtDelta * exchangeRate.udtScale; - if (ckbAllowance >= 0n && udtAllowance >= 0n && gain > bestGain) { + if ( + ckbAllowance >= 0n && + udtAllowance >= 0n && + (partials.length === 0 || gain > 0n) && + gain > bestGain + ) { best = { ckbDelta, udtDelta, partials }; bestGain = gain; } } } - return best ?? { ckbDelta: 0n, udtDelta: 0n, partials: [] }; + return best; } function hasUniquePartialOrderOutPoints(partials: Match["partials"]): boolean { diff --git a/packages/order/src/order.ts b/packages/order/src/order.ts index 3b8c2e37..0358ae44 100644 --- a/packages/order/src/order.ts +++ b/packages/order/src/order.ts @@ -387,22 +387,24 @@ export class OrderManager implements ScriptDeps { ); let best = { - i: -1, - j: -1, ckbDelta: 0n, udtDelta: 0n, partials: [] as Match["partials"], ckbAllowance: allowance.ckbValue, udtAllowance: allowance.udtValue, + gain: 0n, + }; + let advance = { + i: -1, + j: -1, gain: -1n << 256n, }; - // best.i/best.j are offsets into the current two-entry frontiers; (0, 0) + // advance.i/advance.j are offsets into the current two-entry frontiers; (0, 0) // means the current frontier head is already optimal, so the search stops. - while (best.i !== 0 || best.j !== 0) { - ckb2UdtMatches.next(best.i); - udt2CkbMatches.next(best.j); - best.i = 0; - best.j = 0; + while (advance.i !== 0 || advance.j !== 0) { + ckb2UdtMatches.next(advance.i); + udt2CkbMatches.next(advance.j); + advance = { i: 0, j: 0, gain: -1n << 256n }; for (const [i, c2u] of ckb2UdtMatches.buffer.entries()) { for (const [j, u2c] of udt2CkbMatches.buffer.entries()) { @@ -431,18 +433,20 @@ export class OrderManager implements ScriptDeps { continue; } diagnostics.candidates.viable += 1; + if (gain > advance.gain) { + advance = { i, j, gain }; + } if (partials.length > 0) { if (gain > 0n) { diagnostics.candidates.positiveGain += 1; } else { diagnostics.candidates.rejected.nonPositiveGain += 1; + continue; } } if (gain > best.gain) { best = { - i, - j, ckbDelta, udtDelta, partials, From b13aeb6a6973fecf97a0d8759464e8354e0975e3 Mon Sep 17 00:00:00 2001 From: phroi <90913182+phroi@users.noreply.github.com> Date: Wed, 27 May 2026 17:50:57 +0000 Subject: [PATCH 2/3] fix(order): remove stale match state --- packages/order/src/order.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/order/src/order.ts b/packages/order/src/order.ts index 0358ae44..05e42995 100644 --- a/packages/order/src/order.ts +++ b/packages/order/src/order.ts @@ -390,8 +390,6 @@ export class OrderManager implements ScriptDeps { ckbDelta: 0n, udtDelta: 0n, partials: [] as Match["partials"], - ckbAllowance: allowance.ckbValue, - udtAllowance: allowance.udtValue, gain: 0n, }; let advance = { @@ -450,8 +448,6 @@ export class OrderManager implements ScriptDeps { ckbDelta, udtDelta, partials, - ckbAllowance, - udtAllowance, gain, }; } From 828f0d25585fdb9e35f947b78dc9060e3936c388 Mon Sep 17 00:00:00 2001 From: phroi <90913182+phroi@users.noreply.github.com> Date: Wed, 27 May 2026 17:59:52 +0000 Subject: [PATCH 3/3] test(order): simplify match oracle --- packages/order/src/order.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/order/src/order.test.ts b/packages/order/src/order.test.ts index ca21c4ad..682c2d12 100644 --- a/packages/order/src/order.test.ts +++ b/packages/order/src/order.test.ts @@ -1816,7 +1816,6 @@ function exhaustiveSequentialBestMatch( if ( ckbAllowance >= 0n && udtAllowance >= 0n && - (partials.length === 0 || gain > 0n) && gain > bestGain ) { best = { ckbDelta, udtDelta, partials };