mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
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:
+1
-1
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, "")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user