refactor(amm-ui): defer token decimal inference

Keep token balances and amounts in raw integer units until the decimal display model receives dedicated UX analysis.
This commit is contained in:
Ricardo Guilherme Schmidt 2026-07-14 20:10:12 -03:00
parent abbfa08e90
commit 8f24401778
No known key found for this signature in database
GPG Key ID: 1396EA17DE132FFE
4 changed files with 46 additions and 74 deletions

View File

@ -46,9 +46,6 @@ transaction ID is displayed.
scales either deposit from the minimum that mints more than
`MINIMUM_LIQUIDITY`.
- Active Pool flow keeps the reserve ratio and previews expected LP output.
- Decimal inference keeps small supplies at 0 decimals, medium supplies at 6,
large supplies at 9, and massive supplies at 18.
- Human amount parse/format preserves 0, 6, 9, and 18 decimal values.
- `new_definition` and `add_liquidity` account lists match the committed AMM IDL
order.
- Submit re-quotes and surfaces `quote_changed` when the request no longer

View File

@ -133,19 +133,6 @@ function pow10(exponent) {
return value
}
function implyDecimals(totalSupplyRaw) {
if (!isUnsigned(totalSupplyRaw))
return 0
var digits = normalize(totalSupplyRaw).length
if (digits <= 9)
return 0
if (digits <= 14)
return 6
if (digits <= 21)
return 9
return 18
}
function parseHuman(text, decimals) {
var value = String(text)
if (value.length === 0)
@ -163,17 +150,6 @@ function parseHuman(text, decimals) {
return { "ok": true, "code": "", "raw": raw }
}
function trimHumanPrecision(text, decimals) {
var value = String(text)
var match = /^(0|[1-9][0-9]*)(?:\.([0-9]*))?$/.exec(value)
if (!match)
return value
var fraction = match[2] || ""
if (fraction.length <= decimals)
return value
return decimals > 0 ? match[1] + "." + fraction.slice(0, decimals) : match[1]
}
function formatRaw(rawValue, decimals) {
if (!isUnsigned(rawValue))
return ""

View File

@ -65,8 +65,8 @@ AmmActionCard {
? root.newPositionContext.feeTiers : []
readonly property var tokenA: root.tokenById(root.selectedTokenAId)
readonly property var tokenB: root.tokenById(root.selectedTokenBId)
readonly property int decimalsA: AmountMath.implyDecimals(root.tokenA.totalSupplyRaw || "0")
readonly property int decimalsB: AmountMath.implyDecimals(root.tokenB.totalSupplyRaw || "0")
readonly property int decimalsA: 0
readonly property int decimalsB: 0
readonly property bool displayIsCanonical: root.selectedTokenAId.length > 0
&& AmountMath.compareBase58Ids(root.selectedTokenAId,
root.selectedTokenBId) > 0
@ -770,11 +770,9 @@ AmmActionCard {
return
}
var decimals = AmountMath.implyDecimals(token.totalSupplyRaw || "0")
root.tokenResolutionMessage = qsTr("%1 - supply %2 (%3 implied decimals)")
root.tokenResolutionMessage = qsTr("%1 - raw supply %2")
.arg(token.name || root.shortId(token.definitionId))
.arg(AmountMath.formatRaw(token.totalSupplyRaw || "0", decimals))
.arg(decimals)
.arg(AmountMath.formatRaw(token.totalSupplyRaw || "0", 0))
root.selectToken(side, token.definitionId)
}
@ -1193,7 +1191,7 @@ AmmActionCard {
"amount_required": qsTr("Enter a value."),
"amount_must_be_positive": qsTr("Value must be greater than zero."),
"invalid_amount_format": qsTr("Use plain dot-decimal format."),
"invalid_amount_precision": qsTr("Too many decimal places for this token."),
"invalid_amount_precision": qsTr("Token amounts must use whole raw units."),
"invalid_raw_amount": qsTr("Value is outside the supported range."),
"amount_exceeds_balance": qsTr("Amount exceeds the selected holding balance."),
"amount_too_low": qsTr("Value is too low for this pool."),
@ -1239,7 +1237,6 @@ AmmActionCard {
function finishActiveAmount(side, value) {
var decimals = side === "A" ? root.decimalsA : root.decimalsB
value = AmountMath.trimHumanPrecision(value, decimals)
if (side === "A")
root.amountA = value
else
@ -1302,7 +1299,6 @@ AmmActionCard {
function finishMissingAmount(side, value) {
var decimals = side === "A" ? root.decimalsA : root.decimalsB
value = AmountMath.trimHumanPrecision(value, decimals)
if (side === "A")
root.amountA = value
else
@ -1483,8 +1479,7 @@ AmmActionCard {
}
function tokenBalanceDetail(token) {
var decimals = AmountMath.implyDecimals(token.totalSupplyRaw || "0")
return qsTr("Available %1").arg(root.balanceText(token, decimals))
return qsTr("Available %1").arg(root.balanceText(token, 0))
}
function shortId(value) {

View File

@ -59,7 +59,7 @@ TestCase {
}
}
function equalDecimalsContext() {
function rawAmountsContext() {
return {
"status": "ready",
"tokens": [
@ -118,15 +118,23 @@ TestCase {
compare(built.request.tokenBId, tokenLow)
}
function test_tokenAmountsUseRawUnits() {
var form = createForm()
compare(form.decimalsA, 0)
compare(form.decimalsB, 0)
compare(form.balanceText(form.tokenA, form.decimalsA), "1000")
compare(form.balanceText(form.tokenB, form.decimalsB), "5000000000")
}
function test_missingPoolKeepsMinimumRatioWhileEditingDeposit() {
var quote = {
"status": "ok",
"tokenAId": tokenHigh,
"tokenBId": tokenLow,
"poolStatus": "missing_pool",
"minimumAmountARaw": "2000000",
"minimumAmountARaw": "2",
"minimumAmountBRaw": "3",
"actualAmountARaw": "2000000",
"actualAmountARaw": "2",
"actualAmountBRaw": "3"
}
var form = createForm()
@ -146,14 +154,14 @@ TestCase {
var built = form.buildQuoteRequest()
verify(built.ok)
compare(built.request.amountARaw, "4000000")
compare(built.request.amountARaw, "4")
compare(built.request.amountBRaw, "6")
}
function test_missingPoolAcceptsLargeDirectAmountsFromEitherSide() {
var form = createTemporaryObject(formComponent, testCase, {
"flowState": flowState(({})),
"newPositionContext": equalDecimalsContext()
"newPositionContext": rawAmountsContext()
})
verify(form)
wait(0)
@ -176,8 +184,8 @@ TestCase {
compare(form.amountB, "100")
var built = form.buildQuoteRequest()
verify(built.ok)
compare(built.request.amountARaw, "100000000")
compare(built.request.amountBRaw, "150000000")
compare(built.request.amountARaw, "100")
compare(built.request.amountBRaw, "150")
compare(built.request.initialPriceRealRaw, "27670116110564327424")
verify(!built.request.hasOwnProperty("depositScaleBps"))
@ -186,14 +194,14 @@ TestCase {
compare(form.amountB, "200")
built = form.buildQuoteRequest()
verify(built.ok)
compare(built.request.amountARaw, "200000000")
compare(built.request.amountBRaw, "300000000")
compare(built.request.amountARaw, "200")
compare(built.request.amountBRaw, "300")
}
function test_missingPoolRoundsPairedAmountAndTrimsInputPrecision() {
function test_missingPoolRoundsPairedRawAmounts() {
var form = createTemporaryObject(formComponent, testCase, {
"flowState": flowState(({})),
"newPositionContext": equalDecimalsContext()
"newPositionContext": rawAmountsContext()
})
verify(form)
wait(0)
@ -204,22 +212,18 @@ TestCase {
"tokenAId": tokenHigh,
"tokenBId": tokenLow,
"poolStatus": "missing_pool",
"minimumAmountARaw": "26",
"minimumAmountBRaw": "39",
"actualAmountARaw": "26",
"actualAmountBRaw": "39"
"minimumAmountARaw": "1",
"minimumAmountBRaw": "1",
"actualAmountARaw": "1",
"actualAmountBRaw": "1"
})
wait(0)
var cases = [
{ "input": "1", "paired": "0.666667", "rawA": "666667", "rawB": "1000000",
"price": "27670102275513189667" },
{ "input": "2", "paired": "1.333333", "rawA": "1333333", "rawB": "2000000",
"price": "27670123028095084447" },
{ "input": "3", "paired": "2", "rawA": "2000000", "rawB": "3000000",
"price": "27670116110564327424" },
{ "input": "4", "paired": "2.666667", "rawA": "2666667", "rawB": "4000000",
"price": "27670112651800245948" }
{ "input": "1", "paired": "1", "rawA": "1", "rawB": "1" },
{ "input": "2", "paired": "1", "rawA": "1", "rawB": "2" },
{ "input": "3", "paired": "2", "rawA": "2", "rawB": "3" },
{ "input": "4", "paired": "3", "rawA": "3", "rawB": "4" }
]
for (var i = 0; i < cases.length; ++i) {
form.finishMissingAmount("A", cases[i].input)
@ -228,13 +232,12 @@ TestCase {
verify(built.ok)
compare(built.request.amountARaw, cases[i].rawA)
compare(built.request.amountBRaw, cases[i].rawB)
compare(built.request.initialPriceRealRaw, cases[i].price)
}
form.finishMissingAmount("A", "1.1234567")
compare(form.amountA, "1.123456")
compare(form.amountB, "0.748971")
verify(form.buildQuoteRequest().ok)
compare(form.amountA, "1.1234567")
verify(!form.buildQuoteRequest().ok)
compare(form.fieldError("amountA"), form.issueText("invalid_amount_precision"))
}
function test_missingPoolUsesTradeStyleInputsWithInlinePrice() {
@ -374,9 +377,9 @@ TestCase {
"tokenAId": tokenHigh,
"tokenBId": tokenLow,
"poolStatus": "active_pool",
"reserveARaw": "2000000",
"reserveARaw": "2",
"reserveBRaw": "10",
"maxAmountARaw": "4000000",
"maxAmountARaw": "4",
"maxAmountBRaw": "20",
"errors": [{
"code": "amount_exceeds_balance",
@ -404,9 +407,9 @@ TestCase {
"tokenAId": tokenHigh,
"tokenBId": tokenLow,
"poolStatus": "active_pool",
"reserveARaw": "2000000",
"reserveARaw": "2",
"reserveBRaw": "10",
"maxAmountARaw": "4000000",
"maxAmountARaw": "4",
"maxAmountBRaw": "20"
}
var form = createForm()
@ -428,11 +431,12 @@ TestCase {
var built = form.buildQuoteRequest()
verify(built.ok)
compare(built.request.maxAmountARaw, "1000000")
compare(built.request.maxAmountARaw, "1")
compare(built.request.maxAmountBRaw, "5")
form.finishActiveAmount("B", "1.1234567")
compare(form.amountB, "1.123456")
compare(form.amountB, "1.1234567")
verify(!form.buildQuoteRequest().ok)
}
function test_activePoolEditRecoversAfterInvalidQuote() {
@ -443,9 +447,9 @@ TestCase {
"tokenBId": tokenLow,
"poolStatus": "active_pool",
"poolFeeBps": 30,
"reserveARaw": "2000000",
"reserveARaw": "2",
"reserveBRaw": "10",
"maxAmountARaw": "4000000",
"maxAmountARaw": "4",
"maxAmountBRaw": "20"
})
wait(0)
@ -466,7 +470,7 @@ TestCase {
var built = form.buildQuoteRequest()
verify(built.ok)
compare(built.request.maxAmountARaw, "1000000")
compare(built.request.maxAmountARaw, "1")
compare(built.request.maxAmountBRaw, "5")
}