refactor(amm): drop the Raw suffix from amount/value field names

The `*Raw` suffix on the module's amount/price/balance/LP fields was
redundant — every such field is already a base-unit integer, and there was
no formatted sibling to disambiguate from. Drop it across the whole wire
contract in lockstep: the amm_ffi request/response fields (snake_case
`amount_in_raw` → `amount_in`, serde `rename_all="camelCase"` keeps the JSON
keys mapped), the C++ module API, the QtRO `.rep`, the QML/app that consumes
it, the mjs tests, and the module README.

Examples: expectedOutRaw→expectedOut, minReceivedRaw→minReceived,
maxInRaw→maxIn, requiredInRaw→requiredIn, priceRaw→price, reserve{A,B}Raw→
reserve{A,B}, amount{In,Out}Raw→amount{In,Out}, expectedLpRaw→expectedLp,
lpAmountRaw→lpAmount, {max,min,minimum,actual}Amount{A,B}Raw, minimumLpRaw,
minLpRaw, selectedBalance*Raw, totalSupplyRaw, quote*Raw. This also unifies a
pre-existing inconsistency where resolvePoolAccount already emitted `reserveA`
and resolveTokens already emitted `balance`.

Kept where a formatted UI sibling of the same base name exists, so `Raw`
still disambiguates the base-unit value: amountARaw / amountBRaw (vs the
user-input `amountA`/`amountB`), balanceRaw (vs display `balance`), and
initialPriceRaw (vs formatted `initialPrice`). Also kept the format-boundary
helpers formatRaw / rawLpText / probeRaw / displayRaw / displayQuoteRaw /
boundRaw.

BREAKING: the `amm_module` public API field names change (logoscore /
Basecamp / QtRO consumers must update).
This commit is contained in:
r4bbit
2026-08-21 08:11:58 +02:00
parent 78edd23b5b
commit 4cb7c7e51c
18 changed files with 481 additions and 472 deletions
+1 -1
View File
@@ -296,7 +296,7 @@ app through a QML inspector: each test connects to the inspector's TCP server,
finds elements, clicks them, and asserts on the resulting state. `swap.mjs`
selects two tokens, enters a sell amount, submits a swap end-to-end, and then
verifies the pool reserves actually changed on-chain (read back from the
sequencer via the app's `resolvePool`).
sequencer via the app's `resolvePoolAccount`).
> **For the fully isolated, script-driven test flow** (a dedicated wallet from a
> fixed mnemonic + auto-created pool + isolated token config, touching nothing in
@@ -29,8 +29,8 @@ AmmActionCard {
property var holdings: []
readonly property string selectedHoldingAId: tokenAInput.selectedHoldingId
readonly property string selectedHoldingBId: tokenBInput.selectedHoldingId
readonly property string selectedBalanceARaw: tokenAInput.selectedBalanceRaw
readonly property string selectedBalanceBRaw: tokenBInput.selectedBalanceRaw
readonly property string selectedBalanceA: tokenAInput.selectedBalance
readonly property string selectedBalanceB: tokenBInput.selectedBalance
property string selectedTokenAId: ""
property string selectedTokenBId: ""
property int selectedFeeBps: 30
@@ -39,8 +39,8 @@ AmmActionCard {
property string amountB: ""
property string priceAmountA: "1"
property string priceAmountB: "1"
property string minimumAmountARaw: ""
property string minimumAmountBRaw: ""
property string minimumAmountA: ""
property string minimumAmountB: ""
property var localErrors: []
property string resolvingTokenId: ""
property string resolvingTokenSide: ""
@@ -144,7 +144,7 @@ AmmActionCard {
// Per-side funding check, decoupled from buildQuoteRequest/the quote: the deposit each side
// spends must fit its selected holding's balance (the lean createPoolQuote / addLiquidityQuote
// ops never compare amount to balance, so a submit would otherwise fail on an
// insufficient-balance transfer). amountA / selectedBalanceARaw are both the display token-A
// insufficient-balance transfer). amountA / selectedBalanceA are both the display token-A
// side, so no canonical reorientation is needed.
readonly property bool fundingSufficient: root.fundingError("A").length === 0
&& root.fundingError("B").length === 0
@@ -557,14 +557,14 @@ AmmActionCard {
LabelValueRow {
label: qsTr("Expected LP")
value: root.rawLpText(root.quotePayload.expectedLpRaw)
value: root.rawLpText(root.quotePayload.expectedLp)
}
LabelValueRow {
label: root.activePool ? qsTr("Minimum LP") : qsTr("Locked LP")
value: root.rawLpText(root.activePool
? root.quotePayload.minimumLpRaw
: root.quotePayload.lockedLpRaw)
? root.quotePayload.minimumLp
: root.quotePayload.lockedLp)
}
}
@@ -791,9 +791,9 @@ AmmActionCard {
var amount = root.amountA
root.amountA = root.amountB
root.amountB = amount
var minimum = root.minimumAmountARaw
root.minimumAmountARaw = root.minimumAmountBRaw
root.minimumAmountBRaw = minimum
var minimum = root.minimumAmountA
root.minimumAmountA = root.minimumAmountB
root.minimumAmountB = minimum
var priceAmount = root.priceAmountA
root.priceAmountA = root.priceAmountB
root.priceAmountB = priceAmount
@@ -824,8 +824,8 @@ AmmActionCard {
root.amountB = ""
root.priceAmountA = "1"
root.priceAmountB = "1"
root.minimumAmountARaw = ""
root.minimumAmountBRaw = ""
root.minimumAmountA = ""
root.minimumAmountB = ""
root.localErrors = []
// The pair changed: the pool is unknown until re-resolved. Reset poolExists BEFORE
// requestQuote so activePool is false and the empty-amount short-circuit doesn't fire
@@ -845,8 +845,8 @@ AmmActionCard {
|| !root.quoteMatchesPair()) {
return
}
var reserveA = String(root.quotePayload.reserveARaw || "")
var reserveB = String(root.quotePayload.reserveBRaw || "")
var reserveA = String(root.quotePayload.reserveA || "")
var reserveB = String(root.quotePayload.reserveB || "")
if (AmountMath.isUnsigned(reserveA) && reserveA !== "0"
&& AmountMath.isUnsigned(reserveB) && reserveB !== "0") {
root.activePoolQuote = root.quotePayload
@@ -934,8 +934,8 @@ AmmActionCard {
if (!parsedB.ok)
errors.push(root.localIssue(parsedB.code, ["amountB"]))
if (errors.length === 0) {
request.maxAmountARaw = root.displayIsCanonical ? parsedA.raw : parsedB.raw
request.maxAmountBRaw = root.displayIsCanonical ? parsedB.raw : parsedA.raw
request.maxAmountA = root.displayIsCanonical ? parsedA.raw : parsedB.raw
request.maxAmountB = root.displayIsCanonical ? parsedB.raw : parsedA.raw
request.slippageBps = root.slippageBps
}
} else {
@@ -947,7 +947,7 @@ AmmActionCard {
root.displayIsCanonical)
if (!price.ok)
errors.push(root.localIssue(price.code, ["initialPrice"]))
if (root.missingPool && root.minimumAmountARaw.length > 0) {
if (root.missingPool && root.minimumAmountA.length > 0) {
var parsedMissingA = AmountMath.parseHuman(root.amountA, root.decimalsA)
var parsedMissingB = AmountMath.parseHuman(root.amountB, root.decimalsB)
if (!parsedMissingA.ok)
@@ -955,9 +955,9 @@ AmmActionCard {
if (!parsedMissingB.ok)
errors.push(root.localIssue(parsedMissingB.code, ["amountB"]))
if (parsedMissingA.ok && parsedMissingB.ok) {
if (AmountMath.compare(parsedMissingA.raw, root.minimumAmountARaw) < 0)
if (AmountMath.compare(parsedMissingA.raw, root.minimumAmountA) < 0)
errors.push(root.localIssue("amount_too_low", ["amountA"]))
if (AmountMath.compare(parsedMissingB.raw, root.minimumAmountBRaw) < 0)
if (AmountMath.compare(parsedMissingB.raw, root.minimumAmountB) < 0)
errors.push(root.localIssue("amount_too_low", ["amountB"]))
var pairedB = AmountMath.pairAmount(parsedMissingA.raw,
true,
@@ -981,9 +981,9 @@ AmmActionCard {
errors.push(root.localIssue("deposit_ratio_mismatch", ["amountA", "amountB"]))
}
if (errors.length === 0) {
request.amountARaw = root.displayIsCanonical
request.amountA = root.displayIsCanonical
? parsedMissingA.raw : parsedMissingB.raw
request.amountBRaw = root.displayIsCanonical
request.amountB = root.displayIsCanonical
? parsedMissingB.raw : parsedMissingA.raw
var actualPrice = AmountMath.ratioToQ64(root.amountA,
root.amountB,
@@ -991,7 +991,7 @@ AmmActionCard {
root.canonicalDecimalsB,
root.displayIsCanonical)
if (actualPrice.ok) {
request.priceRaw = actualPrice.raw
request.price = actualPrice.raw
priceFromAmounts = true
} else {
errors.push(root.localIssue(actualPrice.code, ["initialPrice"]))
@@ -1000,13 +1000,13 @@ AmmActionCard {
}
}
if (price.ok && !priceFromAmounts)
request.priceRaw = price.raw
request.price = price.raw
if (!root.missingPool) {
var probeA = root.probeRaw(root.tokenA, root.decimalsA)
var probeB = root.probeRaw(root.tokenB, root.decimalsB)
request.maxAmountARaw = root.displayIsCanonical ? probeA : probeB
request.maxAmountBRaw = root.displayIsCanonical ? probeB : probeA
request.maxAmountA = root.displayIsCanonical ? probeA : probeB
request.maxAmountB = root.displayIsCanonical ? probeB : probeA
request.slippageBps = root.slippageBps
}
}
@@ -1047,8 +1047,8 @@ AmmActionCard {
probe[field] = request[field]
var amountA = root.probeRaw(root.tokenA, root.decimalsA)
var amountB = root.probeRaw(root.tokenB, root.decimalsB)
probe.maxAmountARaw = root.displayIsCanonical ? amountA : amountB
probe.maxAmountBRaw = root.displayIsCanonical ? amountB : amountA
probe.maxAmountA = root.displayIsCanonical ? amountA : amountB
probe.maxAmountB = root.displayIsCanonical ? amountB : amountA
probe.slippageBps = root.slippageBps
return probe
}
@@ -1066,15 +1066,15 @@ AmmActionCard {
return root.displayIsCanonical ? "tokenAId" : "tokenBId"
if (field === "tokenBId")
return root.displayIsCanonical ? "tokenBId" : "tokenAId"
if (field === "maxAmountARaw")
if (field === "maxAmountA")
return root.displayIsCanonical ? "amountA" : "amountB"
if (field === "maxAmountBRaw")
if (field === "maxAmountB")
return root.displayIsCanonical ? "amountB" : "amountA"
if (field === "amountARaw")
if (field === "amountA")
return root.displayIsCanonical ? "amountA" : "amountB"
if (field === "amountBRaw")
if (field === "amountB")
return root.displayIsCanonical ? "amountB" : "amountA"
if (field === "priceRaw")
if (field === "price")
return "initialPrice"
return field
}
@@ -1088,7 +1088,7 @@ AmmActionCard {
return ""
var amount = side === "A" ? root.amountA : root.amountB
var decimals = side === "A" ? root.decimalsA : root.decimalsB
var balanceRaw = side === "A" ? root.selectedBalanceARaw : root.selectedBalanceBRaw
var balanceRaw = side === "A" ? root.selectedBalanceA : root.selectedBalanceB
var parsed = AmountMath.parseHuman(amount, decimals)
if (parsed.ok && AmountMath.compare(parsed.raw, balanceRaw) > 0)
return "amount_exceeds_balance"
@@ -1238,8 +1238,8 @@ AmmActionCard {
root.priceAmountA = value
else
root.priceAmountB = value
root.minimumAmountARaw = ""
root.minimumAmountBRaw = ""
root.minimumAmountA = ""
root.minimumAmountB = ""
root.amountA = ""
root.amountB = ""
root.noteDraftChanged()
@@ -1299,12 +1299,12 @@ AmmActionCard {
return
if (root.missingPool) {
var rawA = root.displayRaw("actualAmountARaw", "actualAmountBRaw", "A")
var rawB = root.displayRaw("actualAmountARaw", "actualAmountBRaw", "B")
var minimumA = root.displayRaw("minimumAmountARaw", "minimumAmountBRaw", "A")
var minimumB = root.displayRaw("minimumAmountARaw", "minimumAmountBRaw", "B")
root.minimumAmountARaw = minimumA.length > 0 ? minimumA : rawA
root.minimumAmountBRaw = minimumB.length > 0 ? minimumB : rawB
var rawA = root.displayRaw("actualAmountA", "actualAmountB", "A")
var rawB = root.displayRaw("actualAmountA", "actualAmountB", "B")
var minimumA = root.displayRaw("minimumAmountA", "minimumAmountB", "A")
var minimumB = root.displayRaw("minimumAmountA", "minimumAmountB", "B")
root.minimumAmountA = minimumA.length > 0 ? minimumA : rawA
root.minimumAmountB = minimumB.length > 0 ? minimumB : rawB
if (rawA.length > 0 && rawB.length > 0) {
root.amountA = AmountMath.formatRaw(rawA, root.decimalsA)
root.amountB = AmountMath.formatRaw(rawB, root.decimalsB)
@@ -1325,12 +1325,12 @@ AmmActionCard {
}
function poolReserve(side) {
var reserve = root.displayRaw("reserveARaw", "reserveBRaw", side)
var reserve = root.displayRaw("reserveA", "reserveB", side)
if (AmountMath.isUnsigned(reserve) && reserve !== "0")
return reserve
if (!root.quoteMatchesSelectedPair(root.activePoolQuote))
return ""
return root.displayQuoteRaw(root.activePoolQuote, "reserveARaw", "reserveBRaw", side)
return root.displayQuoteRaw(root.activePoolQuote, "reserveA", "reserveB", side)
}
function quoteAmount(canonicalAField, canonicalBField, side) {
@@ -1343,8 +1343,8 @@ AmmActionCard {
}
function depositSummary() {
var amountA = root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "A")
var amountB = root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "B")
var amountA = root.quoteAmount("actualAmountA", "actualAmountB", "A")
var amountB = root.quoteAmount("actualAmountA", "actualAmountB", "B")
return amountA + " + " + amountB
}
@@ -1366,10 +1366,10 @@ AmmActionCard {
}
function activePriceValue() {
var priceRaw = String(root.quotePayload.priceRaw || "")
if (priceRaw.length === 0 && root.quoteMatchesSelectedPair(root.activePoolQuote))
priceRaw = String(root.activePoolQuote.priceRaw || "")
return AmountMath.priceFromQ64(priceRaw,
var price = String(root.quotePayload.price || "")
if (price.length === 0 && root.quoteMatchesSelectedPair(root.activePoolQuote))
price = String(root.activePoolQuote.price || "")
return AmountMath.priceFromQ64(price,
root.canonicalDecimalsA,
root.canonicalDecimalsB,
root.displayIsCanonical)
@@ -1398,7 +1398,7 @@ AmmActionCard {
return {
"request": built.request,
// Canonical-order holdings for the createPool / addLiquidity calls: the
// request's tokenAId/amountARaw are canonical, so holdingAId must be the
// request's tokenAId/amountA are canonical, so holdingAId must be the
// canonical token A's holding too (the module re-canonicalizes as a no-op).
// The user picks these via the per-side account selectors; selectedHoldingA
// is display token A's holding, so it aligns with tokenA the same way.
@@ -1406,12 +1406,12 @@ AmmActionCard {
"holdingBId": String(root.displayIsCanonical ? root.selectedHoldingBId : root.selectedHoldingAId),
// The add path's slippage floor on the LP minted (orientation-independent),
// taken from the active-pool quote; ignored by the create path.
"minLpRaw": String(root.quotePayload.minimumLpRaw || ""),
"minLp": String(root.quotePayload.minimumLp || ""),
"pairText": qsTr("%1 / %2").arg(root.shortTokenName(root.tokenA)).arg(root.shortTokenName(root.tokenB)),
"feeText": root.feeLabel(root.selectedFeeBps),
"depositAText": root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "A"),
"depositBText": root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "B"),
"expectedLpText": root.rawLpText(root.quotePayload.expectedLpRaw),
"depositAText": root.quoteAmount("actualAmountA", "actualAmountB", "A"),
"depositBText": root.quoteAmount("actualAmountA", "actualAmountB", "B"),
"expectedLpText": root.rawLpText(root.quotePayload.expectedLp),
// Confirm-dialog action derives from the resolved pool state (add vs create).
"poolExists": root.activePool
}
@@ -1455,17 +1455,17 @@ AmmActionCard {
function depositScaleValue() {
var parsed = AmountMath.parseHuman(root.amountA, root.decimalsA)
if (!parsed.ok || !AmountMath.isUnsigned(root.minimumAmountARaw)
|| root.minimumAmountARaw === "0"
|| AmountMath.compare(parsed.raw, root.minimumAmountARaw) < 0) {
if (!parsed.ok || !AmountMath.isUnsigned(root.minimumAmountA)
|| root.minimumAmountA === "0"
|| AmountMath.compare(parsed.raw, root.minimumAmountA) < 0) {
return ""
}
return AmountMath.divide(AmountMath.multiply(parsed.raw, "10000"),
root.minimumAmountARaw).quotient
root.minimumAmountA).quotient
}
function minimumAmountText(side) {
var raw = side === "A" ? root.minimumAmountARaw : root.minimumAmountBRaw
var raw = side === "A" ? root.minimumAmountA : root.minimumAmountB
var decimals = side === "A" ? root.decimalsA : root.decimalsB
return raw.length > 0
? qsTr("Min %1").arg(AmountMath.formatRaw(raw, decimals)) : ""
@@ -40,16 +40,16 @@ Popup {
property int slippageBps: 50
// 1..100. Percent rather than a raw amount: it is what the presets and the
// slider both drive, and it keeps "all of it" exact (see lpAmountRaw).
// slider both drive, and it keeps "all of it" exact (see lpAmount).
property int percent: 50
// ── Quote state (backend.removeLiquidityQuote) ───────────────────────────
property bool quoteLoading: false
property string quoteError: ""
property string amountARaw: "0"
property string amountBRaw: "0"
property string minimumAmountARaw: "0"
property string minimumAmountBRaw: "0"
property string amountA: "0"
property string amountB: "0"
property string minimumAmountA: "0"
property string minimumAmountB: "0"
property bool quoteReady: false
property bool submitting: false
@@ -63,12 +63,12 @@ Popup {
// 100% burns the whole balance exactly; anything else floors, so the dust
// stays in the position rather than rounding the request above the balance.
readonly property string lpAmountRaw: root.percent >= 100
readonly property string lpAmount: root.percent >= 100
? AmountMath.normalize(root.lpBalance)
: AmountMath.mulDivFloor(root.lpBalance, String(root.percent), "100")
readonly property bool hasAmount: AmountMath.isUnsigned(root.lpAmountRaw)
&& AmountMath.normalize(root.lpAmountRaw) !== "0"
readonly property bool hasAmount: AmountMath.isUnsigned(root.lpAmount)
&& AmountMath.normalize(root.lpAmount) !== "0"
readonly property bool canSubmit: root.hasAmount
&& root.quoteReady
&& !root.quoteLoading
@@ -97,10 +97,10 @@ Popup {
root.quoteError = ""
root.submitError = ""
root.quoteReady = false
root.amountARaw = "0"
root.amountBRaw = "0"
root.minimumAmountARaw = "0"
root.minimumAmountBRaw = "0"
root.amountA = "0"
root.amountB = "0"
root.minimumAmountA = "0"
root.minimumAmountB = "0"
root.open()
root.requestQuote()
}
@@ -132,7 +132,7 @@ Popup {
root.runtime.watch(root.backend.removeLiquidityQuote({
"tokenAId": root.tokenAId,
"tokenBId": root.tokenBId,
"lpAmountRaw": root.lpAmountRaw,
"lpAmount": root.lpAmount,
"slippageBps": root.slippageBps
}),
function(quote) {
@@ -140,10 +140,10 @@ Popup {
return
root.quoteLoading = false
if (quote && quote.status === "ok") {
root.amountARaw = String(quote.amountARaw || "0")
root.amountBRaw = String(quote.amountBRaw || "0")
root.minimumAmountARaw = String(quote.minimumAmountARaw || "0")
root.minimumAmountBRaw = String(quote.minimumAmountBRaw || "0")
root.amountA = String(quote.amountA || "0")
root.amountB = String(quote.amountB || "0")
root.minimumAmountA = String(quote.minimumAmountA || "0")
root.minimumAmountB = String(quote.minimumAmountB || "0")
root.quoteError = ""
root.quoteReady = true
return
@@ -173,11 +173,11 @@ Popup {
"holdingAId": root.holdingAId,
"holdingBId": root.holdingBId,
"lpHoldingId": root.lpHoldingId,
"lpAmountRaw": root.lpAmountRaw,
"lpAmount": root.lpAmount,
// The floors the quote computed for this exact amount, so the submit
// enforces the slippage the preview promised.
"minAmountARaw": root.minimumAmountARaw,
"minAmountBRaw": root.minimumAmountBRaw,
"minAmountA": root.minimumAmountA,
"minAmountB": root.minimumAmountB,
// u64-max sentinel = no deadline, same as the other submits.
"deadlineMs": "18446744073709551615"
}),
@@ -367,21 +367,21 @@ Popup {
AmountLine {
objectName: "removeReceiveA"
symbol: root.symbolA
amount: root.quoteReady ? root.amountText(root.amountARaw) : qsTr("—")
amount: root.quoteReady ? root.amountText(root.amountA) : qsTr("—")
}
AmountLine {
objectName: "removeReceiveB"
symbol: root.symbolB
amount: root.quoteReady ? root.amountText(root.amountBRaw) : qsTr("—")
amount: root.quoteReady ? root.amountText(root.amountB) : qsTr("—")
}
Text {
Layout.fillWidth: true
visible: root.quoteReady
text: qsTr("At least %1 %2 and %3 %4 after slippage.")
.arg(root.amountText(root.minimumAmountARaw)).arg(root.symbolA)
.arg(root.amountText(root.minimumAmountBRaw)).arg(root.symbolB)
.arg(root.amountText(root.minimumAmountA)).arg(root.symbolA)
.arg(root.amountText(root.minimumAmountB)).arg(root.symbolB)
color: root.theme.colors.textPlaceholder
font.pixelSize: 11
wrapMode: Text.Wrap
@@ -34,8 +34,8 @@ AmmTokenAmountSurface {
? String(root.footerItem.selectedAccountId) : ""
// Base-unit balance of the selected funding holding (the lean quotes don't check funding,
// so the form compares this against the entered amount). "0" when nothing is selected.
readonly property string selectedBalanceRaw: root.footerItem && root.footerItem.selectedBalanceRaw
? String(root.footerItem.selectedBalanceRaw) : "0"
readonly property string selectedBalance: root.footerItem && root.footerItem.selectedBalance
? String(root.footerItem.selectedBalance) : "0"
footer: root.showHoldingSelector ? accountFooter : null
footerHeight: root.footerItem ? root.footerItem.implicitHeight : 0
@@ -95,7 +95,7 @@ AmmTokenAmountSurface {
implicitHeight: footerSelector.implicitHeight
property alias selectedAccountId: footerSelector.selectedAccountId
property alias selectedBalanceRaw: footerSelector.selectedBalanceRaw
property alias selectedBalance: footerSelector.selectedBalance
ProgramAccountSelector {
id: footerSelector
+24 -24
View File
@@ -47,15 +47,15 @@ Rectangle {
// ── Exact-input quote (backend.swapExactInQuote) ────────────────────────
property bool quoteInLoading: false
property string quoteInError: ""
property string quoteExpectedOutRaw: "0"
property string quoteMinReceivedRaw: "0"
property string quoteExpectedOut: "0"
property string quoteMinReceived: "0"
property int quotePriceImpactBps: 0
// ── Exact-output quote (backend.swapExactOutQuote) ──────────────────────
property bool quoteOutLoading: false
property string quoteOutError: ""
property string quoteRequiredInRaw: "0"
property string quoteMaxInRaw: "0"
property string quoteRequiredIn: "0"
property string quoteMaxIn: "0"
property int quoteOutPriceImpactBps: 0
// ── Swap submission (backend.swapExactInput) ────────────────────────────
@@ -162,8 +162,8 @@ Rectangle {
}
function resetQuoteIn() {
root.quoteExpectedOutRaw = "0"
root.quoteMinReceivedRaw = "0"
root.quoteExpectedOut = "0"
root.quoteMinReceived = "0"
root.quotePriceImpactBps = 0
}
@@ -217,8 +217,8 @@ Rectangle {
return
root.quoteInLoading = false
if (quote && quote.status === "ok") {
root.quoteExpectedOutRaw = quote.expectedOutRaw || "0"
root.quoteMinReceivedRaw = quote.minReceivedRaw || "0"
root.quoteExpectedOut = quote.expectedOut || "0"
root.quoteMinReceived = quote.minReceived || "0"
root.quotePriceImpactBps = quote.priceImpactBps || 0
root.quoteInError = ""
} else {
@@ -247,8 +247,8 @@ Rectangle {
}
function resetQuoteOut() {
root.quoteRequiredInRaw = "0"
root.quoteMaxInRaw = "0"
root.quoteRequiredIn = "0"
root.quoteMaxIn = "0"
root.quoteOutPriceImpactBps = 0
}
@@ -311,8 +311,8 @@ Rectangle {
return
root.quoteOutLoading = false
if (quote && quote.status === "ok") {
root.quoteRequiredInRaw = quote.requiredInRaw || "0"
root.quoteMaxInRaw = quote.maxInRaw || "0"
root.quoteRequiredIn = quote.requiredIn || "0"
root.quoteMaxIn = quote.maxIn || "0"
root.quoteOutPriceImpactBps = quote.priceImpactBps || 0
root.quoteOutError = ""
} else {
@@ -348,11 +348,11 @@ Rectangle {
// exact figures shown and submitted come from the raw quote strings directly.
readonly property real parsedSellAmount: editingSide === "sell"
? parsedSellInput
: (Number(root.quoteRequiredInRaw) || 0)
: (Number(root.quoteRequiredIn) || 0)
readonly property real parsedBuyAmount: editingSide === "buy"
? parsedBuyInput
: (Number(root.quoteExpectedOutRaw) || 0)
: (Number(root.quoteExpectedOut) || 0)
readonly property real feeAmount: swapState.feeAmount(parsedSellAmount)
@@ -362,7 +362,7 @@ Rectangle {
// The quote's exact-integer bound, verbatim (no Number()/double round-trip,
// which would lose precision on large u128 values and diverge from execution):
// min received (exact input) or max sent (exact output).
readonly property string boundRaw: editingSide === "sell" ? root.quoteMinReceivedRaw : root.quoteMaxInRaw
readonly property string bound: editingSide === "sell" ? root.quoteMinReceived : root.quoteMaxIn
readonly property string boundSymbol: editingSide === "sell"
? (buyToken ? buyToken.symbol : "")
: (sellToken ? sellToken.symbol : "")
@@ -432,11 +432,11 @@ Rectangle {
// output in the Sell direction.
readonly property string sellDisplay: editingSide === "sell"
? sellInput
: ((root.quoteRequiredInRaw && root.quoteRequiredInRaw !== "0") ? root.quoteRequiredInRaw : "")
: ((root.quoteRequiredIn && root.quoteRequiredIn !== "0") ? root.quoteRequiredIn : "")
readonly property string buyDisplay: editingSide === "buy"
? buyInput
: ((root.quoteExpectedOutRaw && root.quoteExpectedOutRaw !== "0") ? root.quoteExpectedOutRaw : "")
: ((root.quoteExpectedOut && root.quoteExpectedOut !== "0") ? root.quoteExpectedOut : "")
// Confirmation-dialog preview. The typed side is exact; the quoted side and
// the slippage bound come from the quote's exact-integer strings. boundValue
@@ -446,9 +446,9 @@ Rectangle {
return {
"sellToken": sellToken ? sellToken.symbol : "",
"buyToken": buyToken ? buyToken.symbol : "",
"sellAmount": isExactIn ? root.sellInput : root.quoteRequiredInRaw,
"buyAmount": isExactIn ? root.quoteExpectedOutRaw : root.buyInput,
"boundValue": isExactIn ? root.quoteMinReceivedRaw : root.quoteMaxInRaw,
"sellAmount": isExactIn ? root.sellInput : root.quoteRequiredIn,
"buyAmount": isExactIn ? root.quoteExpectedOut : root.buyInput,
"boundValue": isExactIn ? root.quoteMinReceived : root.quoteMaxIn,
"feeAmount": swapState.formatTokenAmount(feeAmount, sellToken ? sellToken.symbol : ""),
"priceImpactPercent": swapState.formatPercent(priceImpactPercent),
"priceImpactPercentValue": priceImpactPercent,
@@ -477,13 +477,13 @@ Rectangle {
var outHolding = root.buyHolding
// The on-chain guard is the quote's exact-integer bound: the exact-input
// floor (minReceivedRaw) or the exact-output ceiling (maxInRaw). The typed
// floor (minReceived) or the exact-output ceiling (maxIn). The typed
// side (sellInput / buyInput) is the exact amount for that direction.
var pending = root.editingSide === "sell"
? root.backend.swapExactInput(inDef, outDef, inHolding, outHolding,
root.sellInput, root.quoteMinReceivedRaw, deadline)
root.sellInput, root.quoteMinReceived, deadline)
: root.backend.swapExactOutput(inDef, outDef, inHolding, outHolding,
root.buyInput, root.quoteMaxInRaw, deadline)
root.buyInput, root.quoteMaxIn, deadline)
logos.watch(pending,
function (txHash) {
@@ -647,7 +647,7 @@ Rectangle {
priceImpactText: swapState.formatPercent(root.priceImpactPercent)
priceImpactPercent: root.priceImpactPercent
boundLabel: root.boundLabel
boundText: root.boundSymbol ? (root.boundRaw + " " + root.boundSymbol) : root.boundRaw
boundText: root.boundSymbol ? (root.bound + " " + root.boundSymbol) : root.bound
}
SlippageToleranceControl {
+25 -25
View File
@@ -129,8 +129,8 @@ QtObject {
root.runtime.watch(root.backend.addLiquidityQuote({
"tokenAId": built.request.tokenAId,
"tokenBId": built.request.tokenBId,
"maxAmountARaw": built.request.maxAmountARaw,
"maxAmountBRaw": built.request.maxAmountBRaw,
"maxAmountA": built.request.maxAmountA,
"maxAmountB": built.request.maxAmountB,
"slippageBps": built.request.slippageBps
}),
function(quote) {
@@ -187,32 +187,32 @@ QtObject {
"status": "ok",
"tokenAId": built.request.tokenAId,
"tokenBId": built.request.tokenBId,
"actualAmountARaw": String(quote.actualAmountARaw || "0"),
"actualAmountBRaw": String(quote.actualAmountBRaw || "0"),
"minimumAmountARaw": String(quote.minimumAmountARaw || "0"),
"minimumAmountBRaw": String(quote.minimumAmountBRaw || "0"),
"expectedLpRaw": String(quote.expectedLpRaw || "0"),
"lockedLpRaw": String(quote.lockedLpRaw || "0"),
"priceRaw": String(quote.priceRaw || "0")
"actualAmountA": String(quote.actualAmountA || "0"),
"actualAmountB": String(quote.actualAmountB || "0"),
"minimumAmountA": String(quote.minimumAmountA || "0"),
"minimumAmountB": String(quote.minimumAmountB || "0"),
"expectedLp": String(quote.expectedLp || "0"),
"lockedLp": String(quote.lockedLp || "0"),
"price": String(quote.price || "0")
}
}
// Maps addLiquidityQuote + the pool read into the quote shape NewPositionForm reads for an
// active pool. Amounts/reserves are in the request's (canonical) order, matching the form's
// displayIsCanonical mapping; minimumLpRaw is the slippage floor the module computed.
// displayIsCanonical mapping; minimumLp is the slippage floor the module computed.
function assembleAddQuote(built, pool, quote) {
return {
"status": "ok",
"tokenAId": built.request.tokenAId,
"tokenBId": built.request.tokenBId,
"actualAmountARaw": String(quote.amountARaw || "0"),
"actualAmountBRaw": String(quote.amountBRaw || "0"),
"expectedLpRaw": String(quote.expectedLpRaw || "0"),
"minimumLpRaw": String(quote.minimumLpRaw || "0"),
"reserveARaw": String(pool.reserveA || "0"),
"reserveBRaw": String(pool.reserveB || "0"),
"actualAmountA": String(quote.amountA || "0"),
"actualAmountB": String(quote.amountB || "0"),
"expectedLp": String(quote.expectedLp || "0"),
"minimumLp": String(quote.minimumLp || "0"),
"reserveA": String(pool.reserveA || "0"),
"reserveB": String(pool.reserveB || "0"),
"poolFeeBps": pool.feeBps,
"priceRaw": String(quote.priceRaw || "0")
"price": String(quote.price || "0")
}
}
@@ -227,12 +227,12 @@ QtObject {
return
}
// Route by pool state: creation (priceRaw is set only on the missing-pool
// Route by pool state: creation (price is set only on the missing-pool
// path) goes through createPool; the active-pool branch through addLiquidity. Both
// mint a fresh LP holding then submit via the lean module ops (hex ids,
// caller-provided accounts). Quoting for both branches is now on the lean ops
// (createPoolQuote / addLiquidityQuote), routed by resolvePool in requestQuoteNow.
if (snapshot.request.priceRaw !== undefined)
if (snapshot.request.price !== undefined)
root.createPool(snapshot)
else
root.addLiquidity(snapshot)
@@ -262,8 +262,8 @@ QtObject {
"holdingAId": snapshot.holdingAId,
"holdingBId": snapshot.holdingBId,
"lpHoldingId": lpHoldingId,
"amountARaw": snapshot.request.amountARaw,
"amountBRaw": snapshot.request.amountBRaw,
"amountA": snapshot.request.amountA,
"amountB": snapshot.request.amountB,
"feeBps": snapshot.request.feeBps,
// u64-max sentinel = no deadline, same as the swap submits.
"deadlineMs": "18446744073709551615"
@@ -291,7 +291,7 @@ QtObject {
// Add liquidity to an existing pool via the new addLiquidity op. Like createPool a fresh
// LP holding receives the minted LP, so create one then submit. The submit reuses the
// addLiquidityQuote result (maxAmounts + minimumLpRaw) carried on the snapshot. No
// addLiquidityQuote result (maxAmounts + minimumLp) carried on the snapshot. No
// confirmation poll yet.
function addLiquidity(snapshot) {
root.runtime.watch(root.backend.createAccountPublic(),
@@ -314,9 +314,9 @@ QtObject {
"holdingAId": snapshot.holdingAId,
"holdingBId": snapshot.holdingBId,
"lpHoldingId": lpHoldingId,
"maxAmountARaw": snapshot.request.maxAmountARaw,
"maxAmountBRaw": snapshot.request.maxAmountBRaw,
"minLpRaw": snapshot.minLpRaw,
"maxAmountA": snapshot.request.maxAmountA,
"maxAmountB": snapshot.request.maxAmountB,
"minLp": snapshot.minLp,
// u64-max sentinel = no deadline, same as the swap submits.
"deadlineMs": "18446744073709551615"
}
+19 -19
View File
@@ -76,7 +76,7 @@ class AmmUiBackend
// (no pool, unreadable AMM_PROGRAM_BIN, bad inputs, or a failed tx).
SLOT(QString swapExactInput(QString defAHex, QString defBHex, QString userInputHoldingHex, QString userOutputHoldingHex, QString amountInDecimal, QString minOutDecimal, QString deadlineDecimal))
// Server-side SwapExactInput preview for (tokenInHex, tokenOutHex): reads the
// pool and returns { status:"ok", error:"", expectedOutRaw, minReceivedRaw,
// pool and returns { status:"ok", error:"", expectedOut, minReceived,
// priceImpactBps }, oriented and priced via the shared on-chain formula.
// amountInDecimal is a decimal-string base-unit amount; slippageBps is basis
// points. On failure { status:"error", error:<code> } — no_pool,
@@ -84,7 +84,7 @@ class AmmUiBackend
// backend_error. Read-only, no submission.
SLOT(QVariantMap swapExactInQuote(QString tokenInHex, QString tokenOutHex, QString amountInDecimal, int slippageBps))
// Server-side SwapExactOutput preview for (tokenInHex, tokenOutHex): reads the
// pool and returns { status:"ok", error:"", requiredInRaw, maxInRaw,
// pool and returns { status:"ok", error:"", requiredIn, maxIn,
// priceImpactBps } — the input needed for the desired output and the slippage
// ceiling on it — oriented and priced via the shared on-chain formula.
// amountOutDecimal is a decimal-string base-unit amount; slippageBps is basis
@@ -107,9 +107,9 @@ class AmmUiBackend
SLOT(QVariantList tokenList())
// Server-side create-pool preview from the two deposit amounts. `request`
// carries { tokenAId, tokenBId, amountARaw, amountBRaw } (ids hex or base58;
// carries { tokenAId, tokenBId, amountA, amountB } (ids hex or base58;
// amounts decimal-string base units). Returns { status:"ok", error:"",
// amountARaw, amountBRaw, expectedLpRaw, lockedLpRaw, initialPriceRaw } — the
// amountA, amountB, expectedLp, lockedLp, initialPrice } — the
// LP the creator receives and the opening price, via the shared on-chain math.
// On failure { status:"error", error:<code> } — invalid_token_id,
// same_token_pair, amount_too_low, amount_required, bad_amount, backend_error.
@@ -117,17 +117,17 @@ class AmmUiBackend
// PDA nor the pricing).
SLOT(QVariantMap createPoolQuote(QVariantMap request))
// Server-side add-liquidity preview from the two max deposit amounts. `request`
// carries { tokenAId, tokenBId, maxAmountARaw, maxAmountBRaw, slippageBps } (ids hex or
// base58). Reads the pool and returns { status:"ok", error:"", amountARaw, amountBRaw
// (the actual ratio-matched deposits, display order), expectedLpRaw, minimumLpRaw (the
// slippage floor on the LP minted — the submit's min_amount_liquidity), priceRaw }. On
// carries { tokenAId, tokenBId, maxAmountA, maxAmountB, slippageBps } (ids hex or
// base58). Reads the pool and returns { status:"ok", error:"", amountA, amountB
// (the actual ratio-matched deposits, display order), expectedLp, minimumLp (the
// slippage floor on the LP minted — the submit's min_amount_liquidity), price }. On
// failure { status:"error", error:<code> } — no_pool, pair_mismatch, invalid_token_id,
// invalid_slippage, amount_too_low, minimum_lp_zero, bad_amount, backend_error.
// Read-only, no submission.
SLOT(QVariantMap addLiquidityQuote(QVariantMap request))
// Submits a NewDefinition transaction creating the pool for the request's pair.
// `request` carries { tokenAId, tokenBId, holdingAId, holdingBId, lpHoldingId,
// amountARaw, amountBRaw, feeBps, deadlineMs } (ids hex or base58; amounts/deadline
// amountA, amountB, feeBps, deadlineMs } (ids hex or base58; amounts/deadline
// decimal strings). A new pool has no existing LP holding, so the caller supplies
// lpHoldingId — a fresh account it created via createAccountPublic(); the backend
// just forwards to the module and creates no wallet accounts here. Returns
@@ -137,28 +137,28 @@ class AmmUiBackend
// wallet_submission_failed, backend_error).
SLOT(QVariantMap createPool(QVariantMap request))
// Submits an AddLiquidity transaction into the request's existing pool. `request`
// carries { tokenAId, tokenBId, holdingAId, holdingBId, lpHoldingId, maxAmountARaw,
// maxAmountBRaw, minLpRaw, deadlineMs } (ids hex or base58; amounts/deadline decimal
// strings). minLpRaw is the slippage floor on the LP minted; lpHoldingId is a fresh
// carries { tokenAId, tokenBId, holdingAId, holdingBId, lpHoldingId, maxAmountA,
// maxAmountB, minLp, deadlineMs } (ids hex or base58; amounts/deadline decimal
// strings). minLp is the slippage floor on the LP minted; lpHoldingId is a fresh
// holding the caller supplies (the flow creates it) to receive the minted LP — the
// backend forwards and creates no wallet accounts. Returns
// { status:"ok", error:"", transactionId:<hex tx hash> } on success, else
// { status:"error", error:<code> } (wallet_unavailable, config_missing,
// invalid_account_id, bad_amount, no_pool, wallet_submission_failed, backend_error).
SLOT(QVariantMap addLiquidity(QVariantMap request))
// Server-side remove-liquidity preview: burning `lpAmountRaw` returns the
// Server-side remove-liquidity preview: burning `lpAmount` returns the
// proportional share of each reserve. `request` carries { tokenAId, tokenBId,
// lpAmountRaw, slippageBps } (ids hex or base58). Reads the pool and returns
// { status:"ok", error:"", amountARaw, amountBRaw (the withdrawals, display
// order), minimumAmountARaw, minimumAmountBRaw (the slippage floors the submit
// enforces), priceRaw }. On failure { status:"error", error:<code> } —
// lpAmount, slippageBps } (ids hex or base58). Reads the pool and returns
// { status:"ok", error:"", amountA, amountB (the withdrawals, display
// order), minimumAmountA, minimumAmountB (the slippage floors the submit
// enforces), price }. On failure { status:"error", error:<code> } —
// no_pool, pair_mismatch, invalid_token_id, invalid_slippage,
// insufficient_pool_liquidity, amount_too_low, minimum_amount_zero,
// bad_amount, config_missing, backend_error. Read-only, no submission.
SLOT(QVariantMap removeLiquidityQuote(QVariantMap request))
// Submits a RemoveLiquidity transaction against the request's pool. `request`
// carries { tokenAId, tokenBId, holdingAId, holdingBId, lpHoldingId, lpAmountRaw,
// minAmountARaw, minAmountBRaw, deadlineMs } (ids hex or base58;
// carries { tokenAId, tokenBId, holdingAId, holdingBId, lpHoldingId, lpAmount,
// minAmountA, minAmountB, deadlineMs } (ids hex or base58;
// amounts/deadline decimal strings). Unlike createPool/addLiquidity nothing
// fresh is created: lpHoldingId is the existing holding burned from, and the
// token a/b holdings receive the withdrawal. minAmount*Raw are the slippage
+66 -66
View File
@@ -44,14 +44,14 @@ TestCase {
{
"definitionId": tokenLow,
"name": "Low",
"totalSupplyRaw": "1000000",
"totalSupply": "1000000",
"balanceRaw": "1000",
"selectable": true
},
{
"definitionId": tokenHigh,
"name": "High",
"totalSupplyRaw": "1000000000000",
"totalSupply": "1000000000000",
"balanceRaw": "5000000000",
"selectable": true
}
@@ -66,14 +66,14 @@ TestCase {
{
"definitionId": tokenLow,
"name": "Sir Mints-a-Lot",
"totalSupplyRaw": "1000000000000",
"totalSupply": "1000000000000",
"balanceRaw": "1000000000",
"selectable": true
},
{
"definitionId": tokenHigh,
"name": "Aurora",
"totalSupplyRaw": "1000000000000",
"totalSupply": "1000000000000",
"balanceRaw": "1000000000",
"selectable": true
}
@@ -148,10 +148,10 @@ TestCase {
"tokenAId": tokenHigh,
"tokenBId": tokenLow,
"poolStatus": "missing_pool",
"minimumAmountARaw": "2",
"minimumAmountBRaw": "3",
"actualAmountARaw": "2",
"actualAmountBRaw": "3"
"minimumAmountA": "2",
"minimumAmountB": "3",
"actualAmountA": "2",
"actualAmountB": "3"
}
var form = createForm()
form.priceAmountA = "3"
@@ -170,8 +170,8 @@ TestCase {
var built = form.buildQuoteRequest()
verify(built.ok)
compare(built.request.amountARaw, "4")
compare(built.request.amountBRaw, "6")
compare(built.request.amountA, "4")
compare(built.request.amountB, "6")
}
function test_missingPoolAcceptsLargeDirectAmountsFromEitherSide() {
@@ -183,10 +183,10 @@ TestCase {
"tokenAId": tokenHigh,
"tokenBId": tokenLow,
"poolStatus": "missing_pool",
"minimumAmountARaw": "26",
"minimumAmountBRaw": "39",
"actualAmountARaw": "26",
"actualAmountBRaw": "39"
"minimumAmountA": "26",
"minimumAmountB": "39",
"actualAmountA": "26",
"actualAmountB": "39"
})
wait(0)
@@ -195,9 +195,9 @@ TestCase {
compare(form.amountB, "100")
var built = form.buildQuoteRequest()
verify(built.ok)
compare(built.request.amountARaw, "100")
compare(built.request.amountBRaw, "150")
compare(built.request.priceRaw, "27670116110564327424")
compare(built.request.amountA, "100")
compare(built.request.amountB, "150")
compare(built.request.price, "27670116110564327424")
verify(!built.request.hasOwnProperty("depositScaleBps"))
form.finishMissingAmount("B", "200")
@@ -205,8 +205,8 @@ TestCase {
compare(form.amountB, "200")
built = form.buildQuoteRequest()
verify(built.ok)
compare(built.request.amountARaw, "200")
compare(built.request.amountBRaw, "300")
compare(built.request.amountA, "200")
compare(built.request.amountB, "300")
}
function test_missingPoolRoundsPairedRawAmounts() {
@@ -218,10 +218,10 @@ TestCase {
"tokenAId": tokenHigh,
"tokenBId": tokenLow,
"poolStatus": "missing_pool",
"minimumAmountARaw": "1",
"minimumAmountBRaw": "1",
"actualAmountARaw": "1",
"actualAmountBRaw": "1"
"minimumAmountA": "1",
"minimumAmountB": "1",
"actualAmountA": "1",
"actualAmountB": "1"
})
wait(0)
@@ -236,8 +236,8 @@ TestCase {
compare(form.amountB, cases[i].paired)
var built = form.buildQuoteRequest()
verify(built.ok)
compare(built.request.amountARaw, cases[i].rawA)
compare(built.request.amountBRaw, cases[i].rawB)
compare(built.request.amountA, cases[i].rawA)
compare(built.request.amountB, cases[i].rawB)
}
form.finishMissingAmount("A", "1.1234567")
@@ -253,10 +253,10 @@ TestCase {
"tokenAId": tokenHigh,
"tokenBId": tokenLow,
"poolStatus": "missing_pool",
"minimumAmountARaw": "2000000",
"minimumAmountBRaw": "3",
"actualAmountARaw": "2000000",
"actualAmountBRaw": "3"
"minimumAmountA": "2000000",
"minimumAmountB": "3",
"actualAmountA": "2000000",
"actualAmountB": "3"
})
wait(0)
@@ -290,10 +290,10 @@ TestCase {
"tokenAId": tokenHigh,
"tokenBId": tokenLow,
"poolStatus": "missing_pool",
"minimumAmountARaw": "2000000",
"minimumAmountBRaw": "3",
"actualAmountARaw": "2000000",
"actualAmountBRaw": "3"
"minimumAmountA": "2000000",
"minimumAmountB": "3",
"actualAmountA": "2000000",
"actualAmountB": "3"
})
wait(0)
var amountAInput = findChild(form, "tokenAAmountInput")
@@ -319,10 +319,10 @@ TestCase {
"tokenAId": tokenHigh,
"tokenBId": tokenLow,
"poolStatus": "missing_pool",
"minimumAmountARaw": "2000000",
"minimumAmountBRaw": "3",
"actualAmountARaw": "2000000",
"actualAmountBRaw": "3"
"minimumAmountA": "2000000",
"minimumAmountB": "3",
"actualAmountA": "2000000",
"actualAmountB": "3"
}
var form = createForm()
form.flowState = flowState(quote)
@@ -336,10 +336,10 @@ TestCase {
"tokenAId": tokenHigh,
"tokenBId": tokenLow,
"poolStatus": "missing_pool",
"minimumAmountARaw": "2000000",
"minimumAmountBRaw": "3",
"actualAmountARaw": "2000000",
"actualAmountBRaw": "3"
"minimumAmountA": "2000000",
"minimumAmountB": "3",
"actualAmountA": "2000000",
"actualAmountB": "3"
},
"contextLoading": false,
"quoteLoading": false,
@@ -357,13 +357,13 @@ TestCase {
"tokenAId": tokenHigh,
"tokenBId": tokenLow,
"poolStatus": "active_pool",
"reserveARaw": "2",
"reserveBRaw": "10",
"maxAmountARaw": "4",
"maxAmountBRaw": "20",
"reserveA": "2",
"reserveB": "10",
"maxAmountA": "4",
"maxAmountB": "20",
"errors": [{
"code": "amount_exceeds_balance",
"blockingFields": ["maxAmountARaw"]
"blockingFields": ["maxAmountA"]
}]
}
var form = createForm()
@@ -387,10 +387,10 @@ TestCase {
"tokenAId": tokenHigh,
"tokenBId": tokenLow,
"poolStatus": "active_pool",
"reserveARaw": "2",
"reserveBRaw": "10",
"maxAmountARaw": "4",
"maxAmountBRaw": "20"
"reserveA": "2",
"reserveB": "10",
"maxAmountA": "4",
"maxAmountB": "20"
}
var form = createForm()
form.flowState = flowState(quote)
@@ -411,8 +411,8 @@ TestCase {
var built = form.buildQuoteRequest()
verify(built.ok)
compare(built.request.maxAmountARaw, "1")
compare(built.request.maxAmountBRaw, "5")
compare(built.request.maxAmountA, "1")
compare(built.request.maxAmountB, "5")
form.finishActiveAmount("B", "1.1234567")
compare(form.amountB, "1.1234567")
@@ -427,10 +427,10 @@ TestCase {
"tokenBId": tokenLow,
"poolStatus": "active_pool",
"poolFeeBps": 30,
"reserveARaw": "2",
"reserveBRaw": "10",
"maxAmountARaw": "4",
"maxAmountBRaw": "20"
"reserveA": "2",
"reserveB": "10",
"maxAmountA": "4",
"maxAmountB": "20"
})
wait(0)
@@ -450,8 +450,8 @@ TestCase {
var built = form.buildQuoteRequest()
verify(built.ok)
compare(built.request.maxAmountARaw, "1")
compare(built.request.maxAmountBRaw, "5")
compare(built.request.maxAmountA, "1")
compare(built.request.maxAmountB, "5")
}
function test_quoteStateChangeDoesNotRequestAnotherQuote() {
@@ -493,8 +493,8 @@ TestCase {
compare(form.amountB, "")
compare(quoteRequestedSpy.count, 1)
verify(quoteRequestedSpy.signalArguments[0][1].ok)
compare(quoteRequestedSpy.signalArguments[0][1].request.maxAmountARaw, "5000000000")
compare(quoteRequestedSpy.signalArguments[0][1].request.maxAmountBRaw, "1000")
compare(quoteRequestedSpy.signalArguments[0][1].request.maxAmountA, "5000000000")
compare(quoteRequestedSpy.signalArguments[0][1].request.maxAmountB, "1000")
}
function test_contextFailureFinishesTokenResolution() {
@@ -539,8 +539,8 @@ TestCase {
var form = createForm()
form.amountA = "12"
form.amountB = "34"
form.minimumAmountARaw = "12"
form.minimumAmountBRaw = "34"
form.minimumAmountA = "12"
form.minimumAmountB = "34"
form.confirmedPoolStatus = "active_pool"
form.newPositionContext = {
@@ -549,14 +549,14 @@ TestCase {
{
"definitionId": tokenHigh,
"name": "High",
"totalSupplyRaw": "1000000000000",
"totalSupply": "1000000000000",
"balanceRaw": "5000000000",
"selectable": true
},
{
"definitionId": tokenThird,
"name": "Third",
"totalSupplyRaw": "1000000",
"totalSupply": "1000000",
"balanceRaw": "100",
"selectable": true
}
@@ -568,8 +568,8 @@ TestCase {
compare(form.selectedTokenBId, tokenHigh)
compare(form.amountA, "")
compare(form.amountB, "")
compare(form.minimumAmountARaw, "")
compare(form.minimumAmountBRaw, "")
compare(form.minimumAmountA, "")
compare(form.minimumAmountB, "")
compare(form.confirmedPoolStatus, "")
}
+2 -2
View File
@@ -104,8 +104,8 @@ async function selectAccount(app, selectorObjectName) {
// max without needing BigInt.
await app.inspector.send("evaluate", {
expression:
"(function(){var r=matchingAccounts,b=r[0],bb=String(valueFor(b,'balanceRaw')||'0');"
+ "for(var i=1;i<r.length;++i){var v=String(valueFor(r[i],'balanceRaw')||'0');"
"(function(){var r=matchingAccounts,b=r[0],bb=String(valueFor(b, 'balanceRaw')||'0');"
+ "for(var i=1;i<r.length;++i){var v=String(valueFor(r[i], 'balanceRaw')||'0');"
+ "if(v.length>bb.length||(v.length===bb.length&&v>bb)){b=r[i];bb=v;}}"
+ "setSelection(accountIdFor(b),false);})()",
objectId: id,
@@ -50,7 +50,7 @@ Item {
|| (root.selectionMode === ProgramAccountSelector.Output
&& root.createNewSelected))
readonly property var selectedAccount: root.accountById(root.selectedAccountId)
readonly property string selectedBalanceRaw: root.selectedAccount
readonly property string selectedBalance: root.selectedAccount
? String(root.valueFor(
root.selectedAccount,
"balanceRaw") || "0")
@@ -72,7 +72,7 @@ Item {
compare(selector.showCombo, false)
compare(selector.hasFunds, true)
compare(selector.ready, true)
compare(selector.selectedBalanceRaw, "120")
compare(selector.selectedBalance, "120")
}
function test_inputMultipleHoldingsRequiresSelection() {
@@ -87,7 +87,7 @@ Item {
selector.setSelection("holding-b", false)
compare(selector.selectedAccountId, "holding-b")
compare(selector.selectedBalanceRaw, "80")
compare(selector.selectedBalance, "80")
compare(selector.ready, true)
}
+32 -23
View File
@@ -16,6 +16,7 @@ transport-independent JSON FFI), and this module sequences those pure ops with
chain I/O delegated to the `logos_execution_zone` wallet module. Its public
methods (the module API is generated from the header) are:
**Reads**
- `resolvePoolAccount(defAHex, defBHex)` — derives the pool PDA and reads/decodes
the pool account (reserves in canonical `a`/`b` order, fee tier). On success
`{ status: "ok", error: "", poolId, defAHex, defBHex, vaultAId, vaultBId,
@@ -23,20 +24,30 @@ methods (the module API is generated from the header) are:
uninitialized pool or one with no liquidity is `{ status: "error", error:
"no_pool", poolId }` (other codes: `no_program_bin`, `amm_not_initialized`,
`bad_config`).
- `swapExactInput(defAHex, defBHex, userInputHoldingHex, userOutputHoldingHex, amountIn, minOut, deadline)`
— submits an on-chain `SwapExactInput` transaction (defA = token in,
defB = token out); returns the tx hash (or empty on failure). See
**Amount / id conventions** below.
- `configAccount()` — decodes the singleton AMM config (authority + the
token/oracle program ids it was initialized with).
- `feeTiers()` — the AMM's supported fee tiers as raw basis points `[1, 5, 30, 100]`.
- `tokenHoldings(walletOpen)` — the connected wallet's fungible token holdings.
- `resolveTokens(request, walletOpen)` — resolves an app-provided set of token
ids into selector rows (definition + wallet holding per id). The lean,
stateless successor to the removed `newPositionContext` path: the app owns the
ids into selector rows (definition + wallet holding per id). The app owns the
id set, so there is no network envelope or process-cached wallet state here.
- `feeTiers()` — the AMM's supported fee tiers as raw basis points.
- `createPoolQuote(request)` / `createPool(request)` and
`addLiquidityQuote(request)` / `addLiquidity(request)` — the add-liquidity
preview (read-only) and submit paths. The submit forwards the app-supplied
fresh LP holding id; the app backend, which owns the wallet keyset, creates
that account.
**Quotes** (read-only pricing; no submit)
- `swapExactInQuote` / `swapExactOutQuote` — price a swap.
- `createPoolQuote` / `addLiquidityQuote` / `removeLiquidityQuote` — price a
liquidity op.
**Submits** (on-chain transactions; return `{ status, error, transactionId }`,
except the two swaps which return a bare tx hash)
- `swapExactInput` / `swapExactOutput` (defA = token in, defB = token out).
- `createPool` / `addLiquidity` / `removeLiquidity` — the create/add paths take a
fresh `lpHoldingId` the app supplies (the module never creates wallet accounts).
- `syncReserves` — permissionless keeper op refreshing stored reserves + TWAP tick.
- `createPriceObservations` / `createOraclePriceAccount` — seed a pool's TWAP feed.
- `transferOwnership` — admin-only `UpdateConfig` handing over the authority.
See **Amount / id conventions** below, and the
[full `logoscore` runbook](../../docs/module/amm.md) for a worked call per method.
## How it fits together
@@ -197,11 +208,11 @@ logoscore load-module amm_module
Every other op reads on-chain through the wallet module's `get_account_public`,
which fails on a null wallet handle (surfacing as an absent pool), so open the
wallet first — `resolvePool` then works:
wallet first — `resolvePoolAccount` then works:
```bash
logoscore call logos_execution_zone open ~/.lee/wallet/wallet_config.json ~/.lee/wallet/storage.json
logoscore call amm_module resolvePool <defA_hex> <defB_hex>
logoscore call amm_module resolvePoolAccount <defA_hex> <defB_hex>
```
`swapExactInput` reuses that open wallet but additionally needs it **synced**
@@ -230,8 +241,8 @@ wallet's `storage.json` may keep a stale `last_synced_block` ahead of the new
chain — transactions then reference dead state and the sequencer rejects them
(reserves don't move). Reset the cursor (`last_synced_block: 0`, keep
`key_chain`/`labels`) and re-`open` + `sync_to_block <height>` to re-sync from
genesis. `resolvePool` is a **live** sequencer read, so the stale cursor doesn't
affect it — but it still needs the wallet **open**: the read goes through the
genesis. `resolvePoolAccount` is a **live** sequencer read, so the stale cursor
doesn't affect it — but it still needs the wallet **open**: the read goes through the
wallet's sequencer connection (not its private keys), which only exists once the
wallet is opened.
@@ -252,11 +263,9 @@ It requires the wallet module built with the byte-string `instruction` param —
the fork pinned as the `logos_execution_zone` input. See
`docs/amm-swap-qtro-serialization-bug.md`.
## Known follow-ups
## Full API runbook
- **`swapExactOutput` is not exposed yet.** The on-chain program supports it
(`amm_core::Instruction::SwapExactOutput`, identical account layout to
`SwapExactInput`), but the client path was only ever built for exact-input:
`amm_ffi` has no exact-output op and neither the UI nor this module has a
`swapExactOutput` method. Adding it is a near-copy of the exact-input path — an
`amm_swap_exact_output_*` op in the crate plus a `swapExactOutput` method here.
The method list above is a curated subset. For a complete, worked `logoscore`
walkthrough of **every** `amm_module` API — reads, swaps, add/remove liquidity,
the keeper `syncReserves`, oracle setup, and admin — see
[`docs/module/amm.md`](../../docs/module/amm.md).
+112 -112
View File
@@ -91,7 +91,7 @@ fn plan_response(
///
/// The opening price *is* the deposit ratio. With **amounts** supplied, the op uses them and
/// derives the price (`spot_price_q64_64`); **price-only** (no amounts), it takes
/// `price_raw` (Q64.64, canonical) and uses `minimum_opening_pair` — the smallest
/// `price` (Q64.64, canonical) and uses `minimum_opening_pair` — the smallest
/// deposit at that price that clears the permanently-locked `MINIMUM_LIQUIDITY`. Either way it
/// also returns that `minimum*` pair (the form validates entered amounts against it) and
/// `expected_lp = floor(sqrt(a·b)) - MINIMUM_LIQUIDITY` (LP is orientation-independent — the
@@ -106,17 +106,17 @@ pub(super) fn create_pool_quote(request: CreatePoolQuoteRequest) -> Result<Value
}
// Amounts define the opening price; without them the price input drives the minimum.
let amounts = if request.amount_a_raw.is_some() || request.amount_b_raw.is_some() {
let amounts = if request.amount_a.is_some() || request.amount_b.is_some() {
Some((
positive_amount(request.amount_a_raw.as_deref())?,
positive_amount(request.amount_b_raw.as_deref())?,
positive_amount(request.amount_a.as_deref())?,
positive_amount(request.amount_b.as_deref())?,
))
} else {
None
};
let price = match amounts {
Some((amount_a, amount_b)) => spot_price_q64_64(amount_a, amount_b),
None => positive_amount(request.price_raw.as_deref())?,
None => positive_amount(request.price.as_deref())?,
};
let (minimum_a, minimum_b) = minimum_opening_pair(price)?;
let (actual_a, actual_b) = amounts.unwrap_or((minimum_a, minimum_b));
@@ -130,13 +130,13 @@ pub(super) fn create_pool_quote(request: CreatePoolQuoteRequest) -> Result<Value
.ok_or("amount_too_low")?;
Ok(json!({
"actualAmountARaw": actual_a.to_string(),
"actualAmountBRaw": actual_b.to_string(),
"minimumAmountARaw": minimum_a.to_string(),
"minimumAmountBRaw": minimum_b.to_string(),
"expectedLpRaw": expected_lp.to_string(),
"lockedLpRaw": MINIMUM_LIQUIDITY.to_string(),
"priceRaw": price.to_string(),
"actualAmountA": actual_a.to_string(),
"actualAmountB": actual_b.to_string(),
"minimumAmountA": minimum_a.to_string(),
"minimumAmountB": minimum_b.to_string(),
"expectedLp": expected_lp.to_string(),
"lockedLp": MINIMUM_LIQUIDITY.to_string(),
"price": price.to_string(),
}))
}
@@ -165,8 +165,8 @@ pub(super) fn create_pool_plan(request: CreatePoolPlanRequest) -> Result<Value,
let holding_b = account_id_from_hex(&request.user_holding_b_id, "user holding B id")?;
let user_lp = account_id_from_hex(&request.user_holding_lp_id, "user LP holding id")?;
let amount_a = positive_amount(request.amount_a_raw.as_deref())?;
let amount_b = positive_amount(request.amount_b_raw.as_deref())?;
let amount_a = positive_amount(request.amount_a.as_deref())?;
let amount_b = positive_amount(request.amount_b.as_deref())?;
if !amm_core::is_supported_fee_tier(u128::from(request.fee_bps)) {
return Err(String::from("invalid_fee_tier"));
}
@@ -220,10 +220,10 @@ pub(super) fn create_pool_plan(request: CreatePoolPlanRequest) -> Result<Value,
/// caller's max amounts to the pool's canonical `(a, b)` order, then run the guest's exact
/// proportional-deposit math (`amm_program::add::add_liquidity`): the ideal→actual clamp
/// and `delta_lp = min(supply·actual_a/reserve_a, supply·actual_b/reserve_b)`. Returns the
/// actual ratio-matched deposits (display order), the LP minted (`expectedLpRaw`), the
/// slippage floor on that LP (`minimumLpRaw = floor(delta_lp · (1 slippage))`, the
/// submit's `min_amount_liquidity` — like the swap quotes' `minReceivedRaw`), and the pool's
/// spot price (`priceRaw`, token B per token A in display order). Errors: `same_token_pair`,
/// actual ratio-matched deposits (display order), the LP minted (`expectedLp`), the
/// slippage floor on that LP (`minimumLp = floor(delta_lp · (1 slippage))`, the
/// submit's `min_amount_liquidity` — like the swap quotes' `minReceived`), and the pool's
/// spot price (`price`, token B per token A in display order). Errors: `same_token_pair`,
/// `no_pool`, `pair_mismatch` (the pool isn't for this pair), `invalid_slippage` (≥ 100%),
/// bad amounts (`amount_required`, `invalid_raw_amount`, `amount_must_be_positive`),
/// `amount_too_low` (the deposit rounds to zero LP), `minimum_lp_zero` (slippage leaves no
@@ -234,8 +234,8 @@ pub(super) fn add_liquidity_quote(request: AddLiquidityQuoteRequest) -> Result<V
if token_a == token_b {
return Err(String::from("same_token_pair"));
}
let max_a = positive_amount(Some(&request.max_amount_a_raw))?;
let max_b = positive_amount(Some(&request.max_amount_b_raw))?;
let max_a = positive_amount(Some(&request.max_amount_a))?;
let max_b = positive_amount(Some(&request.max_amount_b))?;
if u128::from(request.slippage_bps) >= FEE_BPS_DENOMINATOR {
return Err(String::from("invalid_slippage"));
}
@@ -302,11 +302,11 @@ pub(super) fn add_liquidity_quote(request: AddLiquidityQuoteRequest) -> Result<V
let price = spot_price_q64_64(reserve_display_a, reserve_display_b);
Ok(json!({
"amountARaw": display_a.to_string(),
"amountBRaw": display_b.to_string(),
"expectedLpRaw": delta_lp.to_string(),
"minimumLpRaw": minimum_lp.to_string(),
"priceRaw": price.to_string(),
"amountA": display_a.to_string(),
"amountB": display_b.to_string(),
"expectedLp": delta_lp.to_string(),
"minimumLp": minimum_lp.to_string(),
"price": price.to_string(),
}))
}
@@ -329,9 +329,9 @@ pub(super) fn add_liquidity_plan(request: AddLiquidityPlanRequest) -> Result<Val
let holding_b = account_id_from_hex(&request.user_holding_b_id, "user holding B id")?;
let user_lp = account_id_from_hex(&request.user_holding_lp_id, "user LP holding id")?;
let max_a = positive_amount(Some(&request.max_amount_a_raw))?;
let max_b = positive_amount(Some(&request.max_amount_b_raw))?;
let min_lp = positive_amount(Some(&request.min_lp_raw))?;
let max_a = positive_amount(Some(&request.max_amount_a))?;
let max_b = positive_amount(Some(&request.max_amount_b))?;
let min_lp = positive_amount(Some(&request.min_lp))?;
let deadline = parse_u64(&request.deadline_ms, "deadlineMs")?;
// config / pool / current_tick / clock are order-independent PDAs, so derive_pair takes the
@@ -398,7 +398,7 @@ pub(super) fn add_liquidity_plan(request: AddLiquidityPlanRequest) -> Result<Val
))
}
/// Prices removing liquidity: burning `lp_amount_raw` of the pool returns the proportional
/// Prices removing liquidity: burning `lp_amount` of the pool returns the proportional
/// share of each reserve — `withdraw = floor(reserve · lp / supply)`, the same math the guest
/// (`amm_program::remove::remove_liquidity`) runs. `slippage_bps` sets the `minimumAmount*Raw`
/// floors the submit passes as the guest's nonzero `min_amount_to_remove_token_*`. Amounts are
@@ -415,7 +415,7 @@ pub(super) fn remove_liquidity_quote(
if token_a == token_b {
return Err(String::from("same_token_pair"));
}
let lp_amount = positive_amount(Some(&request.lp_amount_raw))?;
let lp_amount = positive_amount(Some(&request.lp_amount))?;
if u128::from(request.slippage_bps) >= FEE_BPS_DENOMINATOR {
return Err(String::from("invalid_slippage"));
}
@@ -481,11 +481,11 @@ pub(super) fn remove_liquidity_quote(
let price = spot_price_q64_64(reserve_display_a, reserve_display_b);
Ok(json!({
"amountARaw": display_a.to_string(),
"amountBRaw": display_b.to_string(),
"minimumAmountARaw": minimum_display_a.to_string(),
"minimumAmountBRaw": minimum_display_b.to_string(),
"priceRaw": price.to_string(),
"amountA": display_a.to_string(),
"amountB": display_b.to_string(),
"minimumAmountA": minimum_display_a.to_string(),
"minimumAmountB": minimum_display_b.to_string(),
"price": price.to_string(),
}))
}
@@ -508,9 +508,9 @@ pub(super) fn remove_liquidity_plan(request: RemoveLiquidityPlanRequest) -> Resu
let holding_b = account_id_from_hex(&request.user_holding_b_id, "user holding B id")?;
let user_lp = account_id_from_hex(&request.user_holding_lp_id, "user LP holding id")?;
let lp_amount = positive_amount(Some(&request.lp_amount_raw))?;
let min_a = positive_amount(Some(&request.min_amount_a_raw))?;
let min_b = positive_amount(Some(&request.min_amount_b_raw))?;
let lp_amount = positive_amount(Some(&request.lp_amount))?;
let min_a = positive_amount(Some(&request.min_amount_a))?;
let min_b = positive_amount(Some(&request.min_amount_b))?;
let deadline = parse_u64(&request.deadline_ms, "deadlineMs")?;
// config / pool / current_tick / clock are order-independent PDAs, so derive_pair takes the
@@ -639,9 +639,9 @@ mod tests {
CreatePoolQuoteRequest {
token_a_id: account_id_hex(token_a),
token_b_id: account_id_hex(token_b),
price_raw: None,
amount_a_raw: Some(String::from("1000000")),
amount_b_raw: Some(String::from("4000000")),
price: None,
amount_a: Some(String::from("1000000")),
amount_b: Some(String::from("4000000")),
}
}
@@ -676,21 +676,21 @@ mod tests {
let value = create_pool_quote(quote_request(token_a, token_b)).unwrap();
// Amounts supplied ⇒ actual == the amounts; the price is derived from them.
assert_eq!(value["actualAmountARaw"], "1000000");
assert_eq!(value["actualAmountBRaw"], "4000000");
assert_eq!(value["lockedLpRaw"], MINIMUM_LIQUIDITY.to_string());
assert_eq!(value["actualAmountA"], "1000000");
assert_eq!(value["actualAmountB"], "4000000");
assert_eq!(value["lockedLp"], MINIMUM_LIQUIDITY.to_string());
// initial_lp = isqrt(1_000_000 * 4_000_000) = 2_000_000; creator LP = minus lock.
let initial_lp = isqrt_product(1_000_000, 4_000_000);
assert_eq!(
value["expectedLpRaw"],
value["expectedLp"],
(initial_lp - MINIMUM_LIQUIDITY).to_string()
);
let price = spot_price_q64_64(1_000_000, 4_000_000);
assert_eq!(value["priceRaw"], price.to_string());
assert_eq!(value["price"], price.to_string());
// The minimum opening deposit for that price is echoed for the form to validate against.
let (min_a, min_b) = minimum_opening_pair(price).unwrap();
assert_eq!(value["minimumAmountARaw"], min_a.to_string());
assert_eq!(value["minimumAmountBRaw"], min_b.to_string());
assert_eq!(value["minimumAmountA"], min_a.to_string());
assert_eq!(value["minimumAmountB"], min_b.to_string());
// Lean preview — no commitment / status / submittability fields.
assert!(value.get("quoteHash").is_none());
assert!(value.get("canSubmit").is_none());
@@ -707,18 +707,18 @@ mod tests {
let value = create_pool_quote(CreatePoolQuoteRequest {
token_a_id: account_id_hex(token_a),
token_b_id: account_id_hex(token_b),
price_raw: Some(price.to_string()),
amount_a_raw: None,
amount_b_raw: None,
price: Some(price.to_string()),
amount_a: None,
amount_b: None,
})
.unwrap();
// Price-only ⇒ the actual deposit is the minimum opening pair for that price.
assert_eq!(value["actualAmountARaw"], min_a.to_string());
assert_eq!(value["actualAmountBRaw"], min_b.to_string());
assert_eq!(value["minimumAmountARaw"], min_a.to_string());
assert_eq!(value["minimumAmountBRaw"], min_b.to_string());
assert_eq!(value["priceRaw"], price.to_string());
assert_eq!(value["actualAmountA"], min_a.to_string());
assert_eq!(value["actualAmountB"], min_b.to_string());
assert_eq!(value["minimumAmountA"], min_a.to_string());
assert_eq!(value["minimumAmountB"], min_b.to_string());
assert_eq!(value["price"], price.to_string());
}
#[test]
@@ -728,10 +728,10 @@ mod tests {
let ab = create_pool_quote(quote_request(token_a, token_b)).unwrap();
// Swap display order and the paired amounts: the LP figure is symmetric.
let mut ba = quote_request(token_b, token_a);
ba.amount_a_raw = Some(String::from("4000000"));
ba.amount_b_raw = Some(String::from("1000000"));
ba.amount_a = Some(String::from("4000000"));
ba.amount_b = Some(String::from("1000000"));
let ba = create_pool_quote(ba).unwrap();
assert_eq!(ab["expectedLpRaw"], ba["expectedLpRaw"]);
assert_eq!(ab["expectedLp"], ba["expectedLp"]);
}
#[test]
@@ -745,8 +745,8 @@ mod tests {
// isqrt(1 * 1) = 1 ≤ MINIMUM_LIQUIDITY ⇒ the pool can't open.
let token_b = AccountId::new([0xBB; 32]);
let mut tiny = quote_request(token, token_b);
tiny.amount_a_raw = Some(String::from("1"));
tiny.amount_b_raw = Some(String::from("1"));
tiny.amount_a = Some(String::from("1"));
tiny.amount_b = Some(String::from("1"));
assert_eq!(create_pool_quote(tiny), Err(String::from("amount_too_low")));
}
@@ -771,8 +771,8 @@ mod tests {
config: valid_config(amm),
token_a_id: account_id_hex(token_a),
token_b_id: account_id_hex(token_b),
amount_a_raw: Some(String::from("1000000")), // deposit for display token_a
amount_b_raw: Some(String::from("4000000")), // deposit for display token_b
amount_a: Some(String::from("1000000")), // deposit for display token_a
amount_b: Some(String::from("4000000")), // deposit for display token_b
fee_bps: 30,
deadline_ms: String::from("1000"),
user_holding_a_id: account_id_hex(holding_a),
@@ -839,8 +839,8 @@ mod tests {
config: read_failed(),
token_a_id: account_id_hex(token),
token_b_id: account_id_hex(token),
amount_a_raw: Some(String::from("1")),
amount_b_raw: Some(String::from("1")),
amount_a: Some(String::from("1")),
amount_b: Some(String::from("1")),
fee_bps: 30,
deadline_ms: String::from("1"),
user_holding_a_id: account_id_hex(token),
@@ -873,39 +873,39 @@ mod tests {
let ab = add_liquidity_quote(AddLiquidityQuoteRequest {
token_a_id: account_id_hex(def_a),
token_b_id: account_id_hex(def_b),
max_amount_a_raw: String::from("10000"),
max_amount_b_raw: String::from("100000"),
max_amount_a: String::from("10000"),
max_amount_b: String::from("100000"),
slippage_bps: 50,
pool_data: pool_hex(&pool),
})
.unwrap();
assert_eq!(ab["amountARaw"], "10000");
assert_eq!(ab["amountBRaw"], "20000");
assert_eq!(ab["expectedLpRaw"], "10000");
// minimumLpRaw = floor(10000 * (10000 - 50) / 10000) = 9950 (slippage floor on LP).
assert_eq!(ab["minimumLpRaw"], "9950");
assert_eq!(ab["amountA"], "10000");
assert_eq!(ab["amountB"], "20000");
assert_eq!(ab["expectedLp"], "10000");
// minimumLp = floor(10000 * (10000 - 50) / 10000) = 9950 (slippage floor on LP).
assert_eq!(ab["minimumLp"], "9950");
assert_eq!(
ab["priceRaw"],
ab["price"],
spot_price_q64_64(1_000_000, 2_000_000).to_string()
);
assert!(ab.get("lockedLpRaw").is_none());
assert!(ab.get("initialPriceRaw").is_none());
assert!(ab.get("lockedLp").is_none());
assert!(ab.get("initialPrice").is_none());
// Reverse display order: the actual amounts and the price flip to display order.
let ba = add_liquidity_quote(AddLiquidityQuoteRequest {
token_a_id: account_id_hex(def_b),
token_b_id: account_id_hex(def_a),
max_amount_a_raw: String::from("100000"),
max_amount_b_raw: String::from("10000"),
max_amount_a: String::from("100000"),
max_amount_b: String::from("10000"),
slippage_bps: 50,
pool_data: pool_hex(&pool),
})
.unwrap();
assert_eq!(ba["amountARaw"], "20000"); // display token def_b side
assert_eq!(ba["amountBRaw"], "10000"); // display token def_a side
assert_eq!(ba["expectedLpRaw"], "10000");
assert_eq!(ba["amountA"], "20000"); // display token def_b side
assert_eq!(ba["amountB"], "10000"); // display token def_a side
assert_eq!(ba["expectedLp"], "10000");
assert_eq!(
ba["priceRaw"],
ba["price"],
spot_price_q64_64(2_000_000, 1_000_000).to_string()
);
}
@@ -928,8 +928,8 @@ mod tests {
AddLiquidityQuoteRequest {
token_a_id: account_id_hex(token_a),
token_b_id: account_id_hex(token_b),
max_amount_a_raw: max_a.into(),
max_amount_b_raw: max_b.into(),
max_amount_a: max_a.into(),
max_amount_b: max_b.into(),
slippage_bps: 50,
pool_data: data,
}
@@ -978,8 +978,8 @@ mod tests {
add_liquidity_quote(AddLiquidityQuoteRequest {
token_a_id: account_id_hex(def_a),
token_b_id: account_id_hex(def_b),
max_amount_a_raw: String::from("10000"),
max_amount_b_raw: String::from("10000"),
max_amount_a: String::from("10000"),
max_amount_b: String::from("10000"),
slippage_bps: 10_000,
pool_data: pool_hex(&pool),
}),
@@ -1027,9 +1027,9 @@ mod tests {
config: valid_config(amm),
token_a_id: ta,
token_b_id: tb,
max_amount_a_raw: ma.to_string(),
max_amount_b_raw: mb.to_string(),
min_lp_raw: String::from("500"),
max_amount_a: ma.to_string(),
max_amount_b: mb.to_string(),
min_lp: String::from("500"),
deadline_ms: String::from("1000"),
user_holding_a_id: ha,
user_holding_b_id: hb,
@@ -1109,9 +1109,9 @@ mod tests {
config: read_failed(),
token_a_id: account_id_hex(token_a),
token_b_id: account_id_hex(token_b),
max_amount_a_raw: String::from("1"),
max_amount_b_raw: String::from("1"),
min_lp_raw: String::from("1"),
max_amount_a: String::from("1"),
max_amount_b: String::from("1"),
min_lp: String::from("1"),
deadline_ms: String::from("1"),
user_holding_a_id: account_id_hex(token_a),
user_holding_b_id: account_id_hex(token_b),
@@ -1154,18 +1154,18 @@ mod tests {
let ab = remove_liquidity_quote(RemoveLiquidityQuoteRequest {
token_a_id: account_id_hex(def_a),
token_b_id: account_id_hex(def_b),
lp_amount_raw: String::from("100000"),
lp_amount: String::from("100000"),
slippage_bps: 50,
pool_data: pool_hex(&pool),
})
.unwrap();
assert_eq!(ab["amountARaw"], "100000"); // floor(1_000_000 * 100_000 / 1_000_000)
assert_eq!(ab["amountBRaw"], "200000"); // floor(2_000_000 * 100_000 / 1_000_000)
// minimum = floor(withdraw * (10000 - 50) / 10000) — the slippage floor per side.
assert_eq!(ab["minimumAmountARaw"], "99500");
assert_eq!(ab["minimumAmountBRaw"], "199000");
assert_eq!(ab["amountA"], "100000"); // floor(1_000_000 * 100_000 / 1_000_000)
assert_eq!(ab["amountB"], "200000"); // floor(2_000_000 * 100_000 / 1_000_000)
// minimum = floor(withdraw * (10000 - 50) / 10000) — the slippage floor per side.
assert_eq!(ab["minimumAmountA"], "99500");
assert_eq!(ab["minimumAmountB"], "199000");
assert_eq!(
ab["priceRaw"],
ab["price"],
spot_price_q64_64(1_000_000, 2_000_000).to_string()
);
@@ -1173,17 +1173,17 @@ mod tests {
let ba = remove_liquidity_quote(RemoveLiquidityQuoteRequest {
token_a_id: account_id_hex(def_b),
token_b_id: account_id_hex(def_a),
lp_amount_raw: String::from("100000"),
lp_amount: String::from("100000"),
slippage_bps: 50,
pool_data: pool_hex(&pool),
})
.unwrap();
assert_eq!(ba["amountARaw"], "200000"); // display token def_b side
assert_eq!(ba["amountBRaw"], "100000"); // display token def_a side
assert_eq!(ba["minimumAmountARaw"], "199000");
assert_eq!(ba["minimumAmountBRaw"], "99500");
assert_eq!(ba["amountA"], "200000"); // display token def_b side
assert_eq!(ba["amountB"], "100000"); // display token def_a side
assert_eq!(ba["minimumAmountA"], "199000");
assert_eq!(ba["minimumAmountB"], "99500");
assert_eq!(
ba["priceRaw"],
ba["price"],
spot_price_q64_64(2_000_000, 1_000_000).to_string()
);
}
@@ -1205,7 +1205,7 @@ mod tests {
RemoveLiquidityQuoteRequest {
token_a_id: account_id_hex(token_a),
token_b_id: account_id_hex(token_b),
lp_amount_raw: lp.into(),
lp_amount: lp.into(),
slippage_bps: 50,
pool_data: data,
}
@@ -1247,7 +1247,7 @@ mod tests {
remove_liquidity_quote(RemoveLiquidityQuoteRequest {
token_a_id: account_id_hex(def_a),
token_b_id: account_id_hex(def_b),
lp_amount_raw: String::from("100000"),
lp_amount: String::from("100000"),
slippage_bps: 10_000,
pool_data: pool_hex(&pool),
}),
@@ -1318,9 +1318,9 @@ mod tests {
config: valid_config(amm),
token_a_id: ta,
token_b_id: tb,
lp_amount_raw: String::from("100000"),
min_amount_a_raw: min_a.to_string(),
min_amount_b_raw: min_b.to_string(),
lp_amount: String::from("100000"),
min_amount_a: min_a.to_string(),
min_amount_b: min_b.to_string(),
deadline_ms: String::from("1000"),
user_holding_a_id: ha,
user_holding_b_id: hb,
@@ -1408,9 +1408,9 @@ mod tests {
config: read_failed(),
token_a_id: account_id_hex(token_a),
token_b_id: account_id_hex(token_b),
lp_amount_raw: String::from("1"),
min_amount_a_raw: String::from("1"),
min_amount_b_raw: String::from("1"),
lp_amount: String::from("1"),
min_amount_a: String::from("1"),
min_amount_b: String::from("1"),
deadline_ms: String::from("1"),
user_holding_a_id: account_id_hex(token_a),
user_holding_b_id: account_id_hex(token_b),
+20 -20
View File
@@ -99,7 +99,7 @@ pub struct ResolvePoolRequest {
pub struct SwapExactInQuoteRequest {
pub token_in_id: String,
pub token_out_id: String,
pub amount_in_raw: String,
pub amount_in: String,
pub slippage_bps: u32,
/// Pool account data (hex Borsh `PoolDefinition`). Empty / undecodable ⇒ the
/// op returns the `no_pool` error.
@@ -111,7 +111,7 @@ pub struct SwapExactInQuoteRequest {
pub struct SwapExactOutQuoteRequest {
pub token_in_id: String,
pub token_out_id: String,
pub amount_out_raw: String,
pub amount_out: String,
pub slippage_bps: u32,
/// Pool account data (hex Borsh `PoolDefinition`). Empty / undecodable ⇒ the
/// op returns the `no_pool` error.
@@ -171,11 +171,11 @@ pub struct CreatePoolQuoteRequest {
/// order). Required only in the price-only mode (no `amount_*_raw`), where it drives the
/// minimum opening deposit; when amounts are supplied the op derives the price from them.
#[serde(default)]
pub price_raw: Option<String>,
pub price: Option<String>,
#[serde(default)]
pub amount_a_raw: Option<String>,
pub amount_a: Option<String>,
#[serde(default)]
pub amount_b_raw: Option<String>,
pub amount_b: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
@@ -190,9 +190,9 @@ pub struct CreatePoolPlanRequest {
pub token_a_id: String,
pub token_b_id: String,
#[serde(default)]
pub amount_a_raw: Option<String>,
pub amount_a: Option<String>,
#[serde(default)]
pub amount_b_raw: Option<String>,
pub amount_b: Option<String>,
pub fee_bps: u32,
pub deadline_ms: String,
pub user_holding_a_id: String,
@@ -208,17 +208,17 @@ pub struct CreatePoolPlanRequest {
pub struct AddLiquidityQuoteRequest {
pub token_a_id: String,
pub token_b_id: String,
pub max_amount_a_raw: String,
pub max_amount_b_raw: String,
/// Slippage tolerance in basis points — the quote returns `minimumLpRaw`, the LP floor
/// the submit accepts (like the swap quotes take `slippageBps` → `minReceivedRaw`).
pub max_amount_a: String,
pub max_amount_b: String,
/// Slippage tolerance in basis points — the quote returns `minimumLp`, the LP floor
/// the submit accepts (like the swap quotes take `slippageBps` → `minReceived`).
#[serde(default)]
pub slippage_bps: u32,
pub pool_data: String,
}
/// Builds the `AddLiquidity` submission — the add counterpart of `CreatePoolPlanRequest`.
/// `min_lp_raw` is the caller's slippage floor on the LP minted (the guest's
/// `min_lp` is the caller's slippage floor on the LP minted (the guest's
/// `min_amount_liquidity`, applied at submit like the swap plans' `min_out`); `pool_data`
/// supplies the stored vault / LP-definition ids the guest asserts against.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
@@ -231,9 +231,9 @@ pub struct AddLiquidityPlanRequest {
pub config: AccountRead,
pub token_a_id: String,
pub token_b_id: String,
pub max_amount_a_raw: String,
pub max_amount_b_raw: String,
pub min_lp_raw: String,
pub max_amount_a: String,
pub max_amount_b: String,
pub min_lp: String,
pub deadline_ms: String,
pub user_holding_a_id: String,
pub user_holding_b_id: String,
@@ -241,7 +241,7 @@ pub struct AddLiquidityPlanRequest {
pub pool_data: String,
}
/// Prices burning `lp_amount_raw` of an existing pool's LP. `slippage_bps` sets the
/// Prices burning `lp_amount` of an existing pool's LP. `slippage_bps` sets the
/// `minimumAmount*Raw` floors the submit enforces (the guest requires both nonzero and
/// `withdraw >= min`). `pool_data` is the hex Borsh `PoolDefinition` (empty ⇒ no pool).
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
@@ -249,7 +249,7 @@ pub struct AddLiquidityPlanRequest {
pub struct RemoveLiquidityQuoteRequest {
pub token_a_id: String,
pub token_b_id: String,
pub lp_amount_raw: String,
pub lp_amount: String,
#[serde(default)]
pub slippage_bps: u32,
pub pool_data: String,
@@ -271,9 +271,9 @@ pub struct RemoveLiquidityPlanRequest {
pub config: AccountRead,
pub token_a_id: String,
pub token_b_id: String,
pub lp_amount_raw: String,
pub min_amount_a_raw: String,
pub min_amount_b_raw: String,
pub lp_amount: String,
pub min_amount_a: String,
pub min_amount_b: String,
pub deadline_ms: String,
pub user_holding_a_id: String,
pub user_holding_b_id: String,
+24 -24
View File
@@ -146,7 +146,7 @@ pub(super) fn swap_exact_in_quote(request: SwapExactInQuoteRequest) -> Result<Va
if token_in == token_out {
return Err(String::from("same_token_pair"));
}
let amount_in = parse_u128(&request.amount_in_raw, "amountInRaw")?;
let amount_in = parse_u128(&request.amount_in, "amountIn")?;
if u128::from(request.slippage_bps) >= FEE_BPS_DENOMINATOR {
return Err(String::from("invalid_slippage"));
}
@@ -192,8 +192,8 @@ pub(super) fn swap_exact_in_quote(request: SwapExactInQuoteRequest) -> Result<Va
let price_impact_bps = price_impact_bps(amount_in, expected_out, reserve_in, reserve_out);
Ok(json!({
"expectedOutRaw": expected_out.to_string(),
"minReceivedRaw": min_received.to_string(),
"expectedOut": expected_out.to_string(),
"minReceived": min_received.to_string(),
"priceImpactBps": price_impact_bps,
}))
}
@@ -214,7 +214,7 @@ pub(super) fn swap_exact_out_quote(request: SwapExactOutQuoteRequest) -> Result<
if token_in == token_out {
return Err(String::from("same_token_pair"));
}
let amount_out = parse_u128(&request.amount_out_raw, "amountOutRaw")?;
let amount_out = parse_u128(&request.amount_out, "amountOut")?;
if amount_out == 0 {
// The guest's exact_output_swap_logic rejects a zero output before any
// transfer, so a zero-output preview would claim an unexecutable quote
@@ -278,8 +278,8 @@ pub(super) fn swap_exact_out_quote(request: SwapExactOutQuoteRequest) -> Result<
};
Ok(json!({
"requiredInRaw": required_in.to_string(),
"maxInRaw": max_in.to_string(),
"requiredIn": required_in.to_string(),
"maxIn": max_in.to_string(),
"priceImpactBps": price_impact_bps,
}))
}
@@ -630,21 +630,21 @@ mod tests {
let ab = swap_exact_in_quote(SwapExactInQuoteRequest {
token_in_id: account_id_hex(def_a),
token_out_id: account_id_hex(def_b),
amount_in_raw: "10000".into(),
amount_in: "10000".into(),
slippage_bps: 50,
pool_data: pool_data_hex(&pool),
})
.unwrap();
// expectedOut comes from the shared on-chain formula (single source of truth).
let (_, expected_out) = swap_exact_in_amounts(10_000, 1_000_000, 2_000_000, 30);
assert_eq!(ab["expectedOutRaw"], expected_out.to_string());
assert_eq!(ab["expectedOut"], expected_out.to_string());
assert_eq!(
ab["minReceivedRaw"],
ab["minReceived"],
(expected_out * (FEE_BPS_DENOMINATOR - 50) / FEE_BPS_DENOMINATOR).to_string()
);
assert!(ab["priceImpactBps"].is_number());
// Only the priced results are echoed — no pool metadata.
assert!(ab.get("reserveInRaw").is_none());
assert!(ab.get("reserveIn").is_none());
assert!(ab.get("feeBps").is_none());
assert!(ab.get("poolStatus").is_none());
@@ -652,13 +652,13 @@ mod tests {
let ba = swap_exact_in_quote(SwapExactInQuoteRequest {
token_in_id: account_id_hex(def_b),
token_out_id: account_id_hex(def_a),
amount_in_raw: "10000".into(),
amount_in: "10000".into(),
slippage_bps: 50,
pool_data: pool_data_hex(&pool),
})
.unwrap();
let (_, expected_out_ba) = swap_exact_in_amounts(10_000, 2_000_000, 1_000_000, 30);
assert_eq!(ba["expectedOutRaw"], expected_out_ba.to_string());
assert_eq!(ba["expectedOut"], expected_out_ba.to_string());
}
#[test]
@@ -668,7 +668,7 @@ mod tests {
let req = |pool_data: String| SwapExactInQuoteRequest {
token_in_id: account_id_hex(def_a),
token_out_id: account_id_hex(def_b),
amount_in_raw: "10000".into(),
amount_in: "10000".into(),
slippage_bps: 50,
pool_data,
};
@@ -706,7 +706,7 @@ mod tests {
let req = |amount: &str| SwapExactInQuoteRequest {
token_in_id: account_id_hex(def_a),
token_out_id: account_id_hex(def_b),
amount_in_raw: amount.into(),
amount_in: amount.into(),
slippage_bps: 50,
pool_data: pool_data_hex(&pool),
};
@@ -722,7 +722,7 @@ mod tests {
Err(String::from("amount_too_small"))
);
// A normal amount above the fee-rounding floor still quotes.
assert!(swap_exact_in_quote(req("10000")).unwrap()["expectedOutRaw"].is_string());
assert!(swap_exact_in_quote(req("10000")).unwrap()["expectedOut"].is_string());
}
#[test]
@@ -744,12 +744,12 @@ mod tests {
let quote = swap_exact_in_quote(SwapExactInQuoteRequest {
token_in_id: account_id_hex(def_a),
token_out_id: account_id_hex(def_b),
amount_in_raw: "2".into(),
amount_in: "2".into(),
slippage_bps: 50,
pool_data: pool_data_hex(&pool),
})
.unwrap();
assert!(quote["expectedOutRaw"].is_string());
assert!(quote["expectedOut"].is_string());
assert!(
quote["priceImpactBps"].as_u64().unwrap()
<= u64::try_from(FEE_BPS_DENOMINATOR).unwrap()
@@ -769,10 +769,10 @@ mod tests {
fees: 30,
..Default::default()
};
let req = |amount_out_raw: &str| SwapExactOutQuoteRequest {
let req = |amount_out: &str| SwapExactOutQuoteRequest {
token_in_id: account_id_hex(def_a),
token_out_id: account_id_hex(def_b),
amount_out_raw: amount_out_raw.into(),
amount_out: amount_out.into(),
slippage_bps: 50,
pool_data: pool_data_hex(&pool),
};
@@ -780,16 +780,16 @@ mod tests {
// Sell A to receive exactly 10_000 B.
let q = swap_exact_out_quote(req("10000")).unwrap();
let (_, required_in) = swap_exact_out_amounts(10_000, 1_000_000, 2_000_000, 30).unwrap();
assert_eq!(q["requiredInRaw"], required_in.to_string());
assert_eq!(q["requiredIn"], required_in.to_string());
// maxIn = required_in * (10000 + 50) / 10000, rounded up.
assert_eq!(
q["maxInRaw"],
q["maxIn"],
(required_in * 10_050).div_ceil(10_000).to_string()
);
assert!(q["priceImpactBps"].is_number());
// Only the input-side results are echoed — no output/reserves.
assert!(q.get("expectedOutRaw").is_none());
assert!(q.get("reserveInRaw").is_none());
assert!(q.get("expectedOut").is_none());
assert!(q.get("reserveIn").is_none());
// Zero requested output is rejected — the guest rejects exact_amount_out
// == 0, so a zero-output preview would claim an unexecutable quote.
@@ -819,7 +819,7 @@ mod tests {
swap_exact_out_quote(SwapExactOutQuoteRequest {
token_in_id: account_id_hex(def_a),
token_out_id: account_id_hex(def_b),
amount_out_raw: "10000".into(),
amount_out: "10000".into(),
slippage_bps: 50,
pool_data: pool_data_hex(&empty_side),
}),
+4 -4
View File
@@ -22,12 +22,12 @@ fn create_pool_surface_is_reexported_from_crate_root() {
let quote = create_pool_quote(CreatePoolQuoteRequest {
token_a_id: "11".repeat(32),
token_b_id: "22".repeat(32),
price_raw: None, // amounts supplied ⇒ the op derives the price
amount_a_raw: Some("1000000".into()),
amount_b_raw: Some("4000000".into()),
price: None, // amounts supplied ⇒ the op derives the price
amount_a: Some("1000000".into()),
amount_b: Some("4000000".into()),
})
.expect("a valid pure create-pool quote should succeed");
assert_eq!(quote["actualAmountARaw"], "1000000");
assert_eq!(quote["actualAmountA"], "1000000");
let _plan: fn(CreatePoolPlanRequest) -> AmmResult = create_pool_plan;
}
+41 -41
View File
@@ -595,14 +595,14 @@ LogosMap AmmModuleImpl::swapExactInQuote(const std::string& token_in_hex,
const FfiResult quoteResult = call(amm_swap_exact_in_quote, json{
{"tokenInId", token_in},
{"tokenOutId", token_out},
{"amountInRaw", amount_in_decimal},
{"amountIn", amount_in_decimal},
{"slippageBps", slippage_bps},
{"poolData", pool_data},
});
if (!quoteResult.ok)
return error(quoteResult.error.empty() ? "backend_error" : quoteResult.error);
// Success: wrap the priced payload { expectedOutRaw, minReceivedRaw,
// Success: wrap the priced payload { expectedOut, minReceived,
// priceImpactBps } in the standard envelope.
LogosMap out = quoteResult.value;
out["status"] = "ok";
@@ -646,14 +646,14 @@ LogosMap AmmModuleImpl::swapExactOutQuote(const std::string& token_in_hex,
const FfiResult quoteResult = call(amm_swap_exact_out_quote, json{
{"tokenInId", token_in},
{"tokenOutId", token_out},
{"amountOutRaw", amount_out_decimal},
{"amountOut", amount_out_decimal},
{"slippageBps", slippage_bps},
{"poolData", pool_data},
});
if (!quoteResult.ok)
return error(quoteResult.error.empty() ? "backend_error" : quoteResult.error);
// Success: wrap { requiredInRaw, maxInRaw, priceImpactBps } in the envelope.
// Success: wrap { requiredIn, maxIn, priceImpactBps } in the envelope.
LogosMap out = quoteResult.value;
out["status"] = "ok";
out["error"] = "";
@@ -856,7 +856,7 @@ LogosMap AmmModuleImpl::createPoolQuote(const LogosMap& request) {
if (token_a.empty() || token_b.empty())
return error("invalid_token_id");
// amountARaw/amountBRaw arrive as a JSON number (CLI) or decimal string (UI);
// amountA/amountB arrive as a JSON number (CLI) or decimal string (UI);
// coerce to canonical decimal strings (rejects floats — see jsonAmountToDecimal).
// If an amount field is present but malformed, return bad_amount; otherwise leave it
// out so the FFI returns amount_required.
@@ -864,30 +864,30 @@ LogosMap AmmModuleImpl::createPoolQuote(const LogosMap& request) {
{"tokenAId", token_a},
{"tokenBId", token_b},
};
// priceRaw is the Q64.64 opening price; used when no amounts are supplied
// price is the Q64.64 opening price; used when no amounts are supplied
// (price-only ⇒ the op returns the minimum opening deposit). Left out if absent.
std::string price_decimal;
if (jsonAmountToDecimal(request.value("priceRaw", json()), price_decimal))
quoteRequest["priceRaw"] = price_decimal;
if (request.contains("amountARaw")) {
if (jsonAmountToDecimal(request.value("price", json()), price_decimal))
quoteRequest["price"] = price_decimal;
if (request.contains("amountA")) {
std::string amount_a_decimal;
if (!jsonAmountToDecimal(request.at("amountARaw"), amount_a_decimal))
if (!jsonAmountToDecimal(request.at("amountA"), amount_a_decimal))
return error("bad_amount");
quoteRequest["amountARaw"] = amount_a_decimal;
quoteRequest["amountA"] = amount_a_decimal;
}
if (request.contains("amountBRaw")) {
if (request.contains("amountB")) {
std::string amount_b_decimal;
if (!jsonAmountToDecimal(request.at("amountBRaw"), amount_b_decimal))
if (!jsonAmountToDecimal(request.at("amountB"), amount_b_decimal))
return error("bad_amount");
quoteRequest["amountBRaw"] = amount_b_decimal;
quoteRequest["amountB"] = amount_b_decimal;
}
const FfiResult quoteResult = call(amm_create_pool_quote, quoteRequest);
if (!quoteResult.ok)
return error(quoteResult.error.empty() ? "backend_error" : quoteResult.error);
// Success: wrap { actualAmountARaw, actualAmountBRaw, minimumAmountARaw,
// minimumAmountBRaw, expectedLpRaw, lockedLpRaw, priceRaw } in the envelope.
// Success: wrap { actualAmountA, actualAmountB, minimumAmountA,
// minimumAmountB, expectedLp, lockedLp, price } in the envelope.
LogosMap out = quoteResult.value;
out["status"] = "ok";
out["error"] = "";
@@ -928,8 +928,8 @@ LogosMap AmmModuleImpl::createPool(const LogosMap& request) {
std::string amount_a_decimal;
std::string amount_b_decimal;
std::string deadline_decimal;
if (!jsonAmountToDecimal(request.value("amountARaw", json()), amount_a_decimal)
|| !jsonAmountToDecimal(request.value("amountBRaw", json()), amount_b_decimal)
if (!jsonAmountToDecimal(request.value("amountA", json()), amount_a_decimal)
|| !jsonAmountToDecimal(request.value("amountB", json()), amount_b_decimal)
|| !jsonAmountToDecimal(request.value("deadlineMs", json()), deadline_decimal))
return error("bad_amount");
@@ -947,8 +947,8 @@ LogosMap AmmModuleImpl::createPool(const LogosMap& request) {
{"config", config},
{"tokenAId", token_a},
{"tokenBId", token_b},
{"amountARaw", amount_a_decimal},
{"amountBRaw", amount_b_decimal},
{"amountA", amount_a_decimal},
{"amountB", amount_b_decimal},
{"feeBps", fee_val},
{"deadlineMs", deadline_decimal},
{"userHoldingAId", holding_a},
@@ -996,8 +996,8 @@ LogosMap AmmModuleImpl::addLiquidityQuote(const LogosMap& request) {
std::string max_a_decimal;
std::string max_b_decimal;
if (!jsonAmountToDecimal(request.value("maxAmountARaw", json()), max_a_decimal)
|| !jsonAmountToDecimal(request.value("maxAmountBRaw", json()), max_b_decimal))
if (!jsonAmountToDecimal(request.value("maxAmountA", json()), max_a_decimal)
|| !jsonAmountToDecimal(request.value("maxAmountB", json()), max_b_decimal))
return error("bad_amount");
// Derive the pool id (config-free) and read the pool account; its raw data is handed
@@ -1013,7 +1013,7 @@ LogosMap AmmModuleImpl::addLiquidityQuote(const LogosMap& request) {
const std::string pool_data = jStr(pool.value("account", json::object()), "data");
// slippageBps is a fraction of 100% in basis points; the pricing op uses it to derive
// minimumLpRaw (the LP floor the submit accepts). Require an integer JSON number and reject
// minimumLp (the LP floor the submit accepts). Require an integer JSON number and reject
// everything else with a stable invalid_slippage: is_number() would also accept a float
// (and get<int64_t>() on a number_float THROWS, terminating the module), while a string /
// bool would otherwise fall through to a silent 0. A missing field defaults to 0 (no
@@ -1028,15 +1028,15 @@ LogosMap AmmModuleImpl::addLiquidityQuote(const LogosMap& request) {
const FfiResult quoteResult = call(amm_add_liquidity_quote, json{
{"tokenAId", token_a},
{"tokenBId", token_b},
{"maxAmountARaw", max_a_decimal},
{"maxAmountBRaw", max_b_decimal},
{"maxAmountA", max_a_decimal},
{"maxAmountB", max_b_decimal},
{"slippageBps", slippage_bps},
{"poolData", pool_data},
});
if (!quoteResult.ok)
return error(quoteResult.error.empty() ? "backend_error" : quoteResult.error);
// Success: wrap { amountARaw, amountBRaw, expectedLpRaw, minimumLpRaw, priceRaw }.
// Success: wrap { amountA, amountB, expectedLp, minimumLp, price }.
LogosMap out = quoteResult.value;
out["status"] = "ok";
out["error"] = "";
@@ -1076,9 +1076,9 @@ LogosMap AmmModuleImpl::addLiquidity(const LogosMap& request) {
std::string max_b_decimal;
std::string min_lp_decimal;
std::string deadline_decimal;
if (!jsonAmountToDecimal(request.value("maxAmountARaw", json()), max_a_decimal)
|| !jsonAmountToDecimal(request.value("maxAmountBRaw", json()), max_b_decimal)
|| !jsonAmountToDecimal(request.value("minLpRaw", json()), min_lp_decimal)
if (!jsonAmountToDecimal(request.value("maxAmountA", json()), max_a_decimal)
|| !jsonAmountToDecimal(request.value("maxAmountB", json()), max_b_decimal)
|| !jsonAmountToDecimal(request.value("minLp", json()), min_lp_decimal)
|| !jsonAmountToDecimal(request.value("deadlineMs", json()), deadline_decimal))
return error("bad_amount");
@@ -1101,9 +1101,9 @@ LogosMap AmmModuleImpl::addLiquidity(const LogosMap& request) {
{"config", config},
{"tokenAId", token_a},
{"tokenBId", token_b},
{"maxAmountARaw", max_a_decimal},
{"maxAmountBRaw", max_b_decimal},
{"minLpRaw", min_lp_decimal},
{"maxAmountA", max_a_decimal},
{"maxAmountB", max_b_decimal},
{"minLp", min_lp_decimal},
{"deadlineMs", deadline_decimal},
{"userHoldingAId", holding_a},
{"userHoldingBId", holding_b},
@@ -1149,7 +1149,7 @@ LogosMap AmmModuleImpl::removeLiquidityQuote(const LogosMap& request) {
return error("config_missing");
std::string lp_amount_decimal;
if (!jsonAmountToDecimal(request.value("lpAmountRaw", json()), lp_amount_decimal))
if (!jsonAmountToDecimal(request.value("lpAmount", json()), lp_amount_decimal))
return error("bad_amount");
// Derive the pool id (config-free) and read the pool account; its raw data is handed to
@@ -1180,14 +1180,14 @@ LogosMap AmmModuleImpl::removeLiquidityQuote(const LogosMap& request) {
const FfiResult quoteResult = call(amm_remove_liquidity_quote, json{
{"tokenAId", token_a},
{"tokenBId", token_b},
{"lpAmountRaw", lp_amount_decimal},
{"lpAmount", lp_amount_decimal},
{"slippageBps", slippage_bps},
{"poolData", pool_data},
});
if (!quoteResult.ok)
return error(quoteResult.error.empty() ? "backend_error" : quoteResult.error);
// Success: wrap { amountARaw, amountBRaw, minimumAmountARaw, minimumAmountBRaw, priceRaw }.
// Success: wrap { amountA, amountB, minimumAmountA, minimumAmountB, price }.
LogosMap out = quoteResult.value;
out["status"] = "ok";
out["error"] = "";
@@ -1227,9 +1227,9 @@ LogosMap AmmModuleImpl::removeLiquidity(const LogosMap& request) {
std::string min_a_decimal;
std::string min_b_decimal;
std::string deadline_decimal;
if (!jsonAmountToDecimal(request.value("lpAmountRaw", json()), lp_amount_decimal)
|| !jsonAmountToDecimal(request.value("minAmountARaw", json()), min_a_decimal)
|| !jsonAmountToDecimal(request.value("minAmountBRaw", json()), min_b_decimal)
if (!jsonAmountToDecimal(request.value("lpAmount", json()), lp_amount_decimal)
|| !jsonAmountToDecimal(request.value("minAmountA", json()), min_a_decimal)
|| !jsonAmountToDecimal(request.value("minAmountB", json()), min_b_decimal)
|| !jsonAmountToDecimal(request.value("deadlineMs", json()), deadline_decimal))
return error("bad_amount");
@@ -1252,9 +1252,9 @@ LogosMap AmmModuleImpl::removeLiquidity(const LogosMap& request) {
{"config", config},
{"tokenAId", token_a},
{"tokenBId", token_b},
{"lpAmountRaw", lp_amount_decimal},
{"minAmountARaw", min_a_decimal},
{"minAmountBRaw", min_b_decimal},
{"lpAmount", lp_amount_decimal},
{"minAmountA", min_a_decimal},
{"minAmountB", min_b_decimal},
{"deadlineMs", deadline_decimal},
{"userHoldingAId", holding_a},
{"userHoldingBId", holding_b},
+21 -21
View File
@@ -66,8 +66,8 @@ public:
LogosMap createOraclePriceAccount(const LogosMap& request);
/// Prices a `SwapExactInput` for the (token_in_hex, token_out_hex) pair:
/// reads the pool and returns `{ status:"ok", error:"", expectedOutRaw,
/// minReceivedRaw, priceImpactBps }`, oriented and computed server-side via
/// reads the pool and returns `{ status:"ok", error:"", expectedOut,
/// minReceived, priceImpactBps }`, oriented and computed server-side via
/// the shared on-chain formula. `amount_in` accepts a JSON integer or a
/// decimal string (JSON floats rejected); `slippage_bps` is basis points.
/// On failure: `{ status:"error", error:<code> }` — `no_pool` (no pool /
@@ -81,8 +81,8 @@ public:
int64_t slippage_bps);
/// Prices a `SwapExactOutput` for the (token_in_hex, token_out_hex) pair:
/// reads the pool and returns `{ status:"ok", error:"", requiredInRaw,
/// maxInRaw, priceImpactBps }`, oriented and computed server-side via the
/// reads the pool and returns `{ status:"ok", error:"", requiredIn,
/// maxIn, priceImpactBps }`, oriented and computed server-side via the
/// shared on-chain formula. `amount_out` accepts a JSON integer or a decimal
/// string (JSON floats rejected); `slippage_bps` is basis points. On failure:
/// `{ status:"error", error:<code> }` — `no_pool` (no pool / liquidity),
@@ -129,10 +129,10 @@ public:
/// Prices creating a pool for (tokenAId, tokenBId) from the two deposit amounts.
/// A pure preview — no chain reads, and no fee needed (the fee is not part of the
/// pool PDA and doesn't affect the opening LP/price). Returns `{ status:"ok",
/// error:"", amountARaw, amountBRaw, expectedLpRaw, lockedLpRaw, initialPriceRaw }`
/// computed via the shared `amm_core` opening-LP math, so `expectedLpRaw` is
/// error:"", amountA, amountB, expectedLp, lockedLp, initialPrice }`
/// computed via the shared `amm_core` opening-LP math, so `expectedLp` is
/// exactly what the guest mints. `request` carries `{ tokenAId, tokenBId,
/// amountARaw, amountBRaw }` (ids hex or base58, normalized to hex; amounts a JSON
/// amountA, amountB }` (ids hex or base58, normalized to hex; amounts a JSON
/// integer or decimal string). On failure: `{ status:"error", error:<code> }` —
/// `invalid_token_id`, `same_token_pair`, `bad_amount` (an amount field is present
/// but not a valid integer — e.g. a float, from `jsonAmountToDecimal`),
@@ -146,7 +146,7 @@ public:
/// Submits a `NewDefinition` transaction creating the pool for the request's pair.
/// `request` carries `{ tokenAId, tokenBId, holdingAId, holdingBId, lpHoldingId,
/// amountARaw, amountBRaw, feeBps, deadlineMs }` (ids hex or base58, normalized to
/// amountA, amountB, feeBps, deadlineMs }` (ids hex or base58, normalized to
/// hex; amounts/deadline a JSON integer or decimal string, deadline a u64 unix-ms).
/// The caller provides `lpHoldingId` — a fresh (empty) account the guest initializes
/// and mints the creator's LP tokens into; a new pool has no pre-existing LP holding,
@@ -162,20 +162,20 @@ public:
/// Prices an `AddLiquidity` into the existing pool for (tokenAId, tokenBId) from the
/// two max deposit amounts. Reads the pool server-side (like the swap quotes) and runs
/// the guest's proportional-deposit math. Returns the same shape as `createPoolQuote`
/// minus the create-only locked LP: `{ status:"ok", error:"", amountARaw, amountBRaw,
/// expectedLpRaw, priceRaw }` — the actual ratio-matched deposits (display order), the
/// minus the create-only locked LP: `{ status:"ok", error:"", amountA, amountB,
/// expectedLp, price }` — the actual ratio-matched deposits (display order), the
/// LP minted, and the pool's spot price. Slippage is applied at submit, not here.
/// `request` carries `{ tokenAId, tokenBId, maxAmountARaw, maxAmountBRaw }` (ids hex or
/// `request` carries `{ tokenAId, tokenBId, maxAmountA, maxAmountB }` (ids hex or
/// base58, normalized to hex; amounts a JSON integer or decimal string). On failure:
/// `{ status:"error", error:<code> }` — `invalid_token_id`, `config_missing`,
/// `bad_amount`, `no_pool`, `pair_mismatch`, `amount_too_low`, or `backend_error`.
LogosMap addLiquidityQuote(const LogosMap& request);
/// Submits an `AddLiquidity` transaction into the request's pool. `request` carries
/// `{ tokenAId, tokenBId, holdingAId, holdingBId, lpHoldingId, maxAmountARaw,
/// maxAmountBRaw, minLpRaw, deadlineMs }` (ids hex or base58, normalized to hex;
/// amounts/deadline a JSON integer or decimal string). `minLpRaw` is the caller's
/// slippage floor on the LP minted (the UI derives it from the quote's expectedLpRaw
/// `{ tokenAId, tokenBId, holdingAId, holdingBId, lpHoldingId, maxAmountA,
/// maxAmountB, minLp, deadlineMs }` (ids hex or base58, normalized to hex;
/// amounts/deadline a JSON integer or decimal string). `minLp` is the caller's
/// slippage floor on the LP minted (the UI derives it from the quote's expectedLp
/// and its slippage control). `lpHoldingId` is the holding that receives the minted LP.
/// On success: `{ status:"ok", error:"", transactionId:<hex tx hash> }`. On failure:
/// `{ status:"error", error:<code> }` — `config_missing`, `backend_error`,
@@ -185,12 +185,12 @@ public:
LogosMap addLiquidity(const LogosMap& request);
/// Prices a `RemoveLiquidity` from the existing pool for (tokenAId, tokenBId): burning
/// `lpAmountRaw` returns the proportional share of each reserve. Reads the pool
/// `lpAmount` returns the proportional share of each reserve. Reads the pool
/// server-side (like the add quote) and runs the guest's `floor(reserve·lp/supply)` math.
/// Returns `{ status:"ok", error:"", amountARaw, amountBRaw, minimumAmountARaw,
/// minimumAmountBRaw, priceRaw }` — the withdrawals (display order), the slippage floors
/// Returns `{ status:"ok", error:"", amountA, amountB, minimumAmountA,
/// minimumAmountB, price }` — the withdrawals (display order), the slippage floors
/// the submit enforces, and the pool's spot price. `request` carries `{ tokenAId, tokenBId,
/// lpAmountRaw, slippageBps }` (ids hex or base58, normalized to hex; amount a JSON integer
/// lpAmount, slippageBps }` (ids hex or base58, normalized to hex; amount a JSON integer
/// or decimal string). On failure: `{ status:"error", error:<code> }` — `invalid_token_id`,
/// `config_missing`, `bad_amount`, `invalid_slippage`, `no_pool`, `pair_mismatch`,
/// `insufficient_pool_liquidity`, `amount_too_low`, `minimum_amount_zero`, or
@@ -198,8 +198,8 @@ public:
LogosMap removeLiquidityQuote(const LogosMap& request);
/// Submits a `RemoveLiquidity` transaction against the request's pool. `request` carries
/// `{ tokenAId, tokenBId, holdingAId, holdingBId, lpHoldingId, lpAmountRaw, minAmountARaw,
/// minAmountBRaw, deadlineMs }` (ids hex or base58, normalized to hex; amounts/deadline a
/// `{ tokenAId, tokenBId, holdingAId, holdingBId, lpHoldingId, lpAmount, minAmountA,
/// minAmountB, deadlineMs }` (ids hex or base58, normalized to hex; amounts/deadline a
/// JSON integer or decimal string). `lpHoldingId` is the existing holding burned; the token
/// a/b holdings receive the withdrawal (no fresh account, unlike add/create). `minAmount*Raw`
/// are the caller's slippage floors on the tokens withdrawn. On success: