mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
feat(amm): let LPs choose the LP-token destination account
Add-liquidity minted a fresh LP account every time, fragmenting a position across holdings. The New position form now has an LP-destination selector (the same Input-mode component as the token funding rows): add-liquidity preselects the wallet's existing LP holding so deposits consolidate, while create-pool has none and mints a fresh one. - addLiquidityQuote returns lpDefinitionId (base58) so the form matches holdings - createPool/addLiquidity submit into the chosen holding, else create-fresh - e2e: add-liquidity waits for the preselect; create-pool asserts fresh-account
This commit is contained in:
@@ -31,6 +31,11 @@ AmmActionCard {
|
||||
readonly property string selectedHoldingBId: tokenBInput.selectedHoldingId
|
||||
readonly property string selectedBalanceA: tokenAInput.selectedBalance
|
||||
readonly property string selectedBalanceB: tokenBInput.selectedBalance
|
||||
// The pool's LP token definition (base58), surfaced by the active-pool add-liquidity
|
||||
// quote, so the LP-destination selector can offer the wallet's existing LP holdings.
|
||||
// Empty for create-pool (no pool yet) — the Output selector then shows create-new only.
|
||||
readonly property string lpDefinitionId: root.quoteMatchesSelectedPair(root.activePoolQuote)
|
||||
? String(root.activePoolQuote.lpDefinitionId || "") : ""
|
||||
property string selectedTokenAId: ""
|
||||
property string selectedTokenBId: ""
|
||||
property int selectedFeeBps: 30
|
||||
@@ -566,6 +571,44 @@ AmmActionCard {
|
||||
? root.quotePayload.minimumLp
|
||||
: root.quotePayload.lockedLp)
|
||||
}
|
||||
|
||||
// LP destination — grouped with the LP figures above. Same Input-mode account
|
||||
// selector as the token funding rows: preselects the wallet's existing LP holding
|
||||
// for this pool (consolidating rather than fragmenting), or shows "New LP account"
|
||||
// when there's none (create-pool, or a pool you don't hold) so NewPositionFlow
|
||||
// mints a fresh one.
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
text: qsTr("Receive LP into")
|
||||
color: root.theme.colors.textSecondary
|
||||
font.pixelSize: 12
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ProgramAccountSelector {
|
||||
id: lpSelector
|
||||
objectName: "lpDestinationSelector"
|
||||
Layout.preferredWidth: Math.round(parent.width / 2)
|
||||
sourceModel: root.holdings
|
||||
accountType: "TokenHolding"
|
||||
stateField: "definitionId"
|
||||
stateValue: root.lpDefinitionId
|
||||
selectionMode: ProgramAccountSelector.Input
|
||||
showWhenSingle: true
|
||||
textAlignment: Text.AlignRight
|
||||
emptyInputText: qsTr("New LP account")
|
||||
accessibleName: qsTr("LP token destination account")
|
||||
backgroundColor: root.theme.colors.panelBg
|
||||
hoverColor: root.theme.colors.panelHoverBg
|
||||
textColor: root.theme.colors.textPrimary
|
||||
secondaryTextColor: root.theme.colors.textSecondary
|
||||
borderColor: root.theme.colors.borderStrong
|
||||
focusColor: root.theme.colors.ctaBg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SubmittedTransaction {
|
||||
@@ -1404,6 +1447,10 @@ AmmActionCard {
|
||||
// is display token A's holding, so it aligns with tokenA the same way.
|
||||
"holdingAId": String(root.displayIsCanonical ? root.selectedHoldingAId : root.selectedHoldingBId),
|
||||
"holdingBId": String(root.displayIsCanonical ? root.selectedHoldingBId : root.selectedHoldingAId),
|
||||
// LP-token destination: an existing LP holding, or a fresh account when createLpNew.
|
||||
// The submit flow (NewPositionFlow) only calls createAccountPublic() when createLpNew.
|
||||
"lpHoldingId": String(lpSelector.selectedAccountId || ""),
|
||||
"createLpNew": lpSelector.createNewSelected,
|
||||
// The add path's slippage floor on the LP minted (orientation-independent),
|
||||
// taken from the active-pool quote; ignored by the create path.
|
||||
"minLp": String(root.quotePayload.minimumLp || ""),
|
||||
|
||||
@@ -212,7 +212,10 @@ QtObject {
|
||||
"reserveA": String(pool.reserveA || "0"),
|
||||
"reserveB": String(pool.reserveB || "0"),
|
||||
"poolFeeBps": pool.feeBps,
|
||||
"price": String(quote.price || "0")
|
||||
"price": String(quote.price || "0"),
|
||||
// The pool's LP token (base58, matching the holdings' definitionId) so the form
|
||||
// can offer the wallet's existing LP holdings as the mint destination.
|
||||
"lpDefinitionId": String(quote.lpDefinitionId || "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,6 +245,12 @@ QtObject {
|
||||
// holding, so the caller provides a fresh account: create one, then submit. No
|
||||
// confirmation poll yet (transactionStatus is pending an upstream dependency).
|
||||
function createPool(snapshot) {
|
||||
// If the user picked an existing LP holding, mint straight into it; otherwise create a
|
||||
// fresh account first. Create-pool has no existing LP, so it always takes the fresh path.
|
||||
if (!snapshot.createLpNew && String(snapshot.lpHoldingId || "").length > 0) {
|
||||
root.submitCreatePool(snapshot, String(snapshot.lpHoldingId))
|
||||
return
|
||||
}
|
||||
root.runtime.watch(root.backend.createAccountPublic(),
|
||||
function(lpId) {
|
||||
if (!lpId || String(lpId).length === 0) {
|
||||
@@ -289,11 +298,15 @@ 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 + minimumLp) carried on the snapshot. No
|
||||
// confirmation poll yet.
|
||||
// Add liquidity to an existing pool via the addLiquidity op. The minted LP goes to the
|
||||
// holding the user chose (createLpNew=false), consolidating an existing position — or to a
|
||||
// fresh account when they opt to create one. The submit reuses the addLiquidityQuote result
|
||||
// (maxAmounts + minimumLp) carried on the snapshot. No confirmation poll yet.
|
||||
function addLiquidity(snapshot) {
|
||||
if (!snapshot.createLpNew && String(snapshot.lpHoldingId || "").length > 0) {
|
||||
root.submitAddLiquidity(snapshot, String(snapshot.lpHoldingId))
|
||||
return
|
||||
}
|
||||
root.runtime.watch(root.backend.createAccountPublic(),
|
||||
function(lpId) {
|
||||
if (!lpId || String(lpId).length === 0) {
|
||||
|
||||
@@ -120,7 +120,9 @@ class AmmUiBackend
|
||||
// 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
|
||||
// slippage floor on the LP minted — the submit's min_amount_liquidity), price,
|
||||
// lpDefinitionId (the pool's LP token, base58 — so the UI can offer existing LP
|
||||
// holdings as the mint destination) }. 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.
|
||||
|
||||
@@ -252,6 +252,22 @@ test("amm liquidity: add to the A/B pool", async (app) => {
|
||||
console.log(` deposit: A=${filled.amountA} B=${filled.amountB}`);
|
||||
await saveShot(app, "add-liquidity-filled");
|
||||
|
||||
// 6b. This wallet seeded the A/B pool, so it already holds LP for it. The LP-destination
|
||||
// selector must therefore preselect that existing holding — the add CONSOLIDATES into
|
||||
// the position rather than minting a fresh account. Wait for the preselection.
|
||||
const lpSelectorId = await idByObjectName(app, "lpDestinationSelector");
|
||||
await app.waitFor(
|
||||
async () => {
|
||||
if ((await prop(app, lpSelectorId, "hasFunds")) !== true)
|
||||
throw new Error("no existing LP holding matched yet");
|
||||
if (!(await prop(app, lpSelectorId, "selectedAccountId")))
|
||||
throw new Error("LP holding not preselected yet");
|
||||
},
|
||||
{ timeout: 15000, interval: 300, description: "existing LP holding preselected" },
|
||||
);
|
||||
const lpHoldingId = await prop(app, lpSelectorId, "selectedAccountId");
|
||||
console.log(` LP consolidates into existing holding ${String(lpHoldingId).slice(0, 8)}…`);
|
||||
|
||||
// 7. Submit -> confirmation dialog -> confirm.
|
||||
const dialogId = await idByObjectName(app, "liquidityConfirmDialog");
|
||||
const submitId = await idByObjectName(app, "newPositionSubmitButton");
|
||||
@@ -285,9 +301,9 @@ test("amm liquidity: add to the A/B pool", async (app) => {
|
||||
await ignore(() => evaluate(app, dialogId, "confirm()"));
|
||||
}
|
||||
|
||||
// 8. Wait for the add to submit. Fully async — createAccountPublic (mint the fresh LP
|
||||
// holding) then addLiquidity then the tx submit — so transactionId lands a few
|
||||
// seconds after confirm().
|
||||
// 8. Wait for the add to submit. An existing LP holding was chosen, so there is no
|
||||
// createAccountPublic step — addLiquidity submits straight into it — and transactionId
|
||||
// lands a couple seconds after confirm().
|
||||
try {
|
||||
await app.waitFor(
|
||||
async () => {
|
||||
|
||||
@@ -217,6 +217,20 @@ test("amm liquidity: create the A/C pool", async (app) => {
|
||||
await saveShot(app, "create-pool-filled");
|
||||
console.log(` minimum deposit: A=${(await formState(app, formId)).amountA} C=${(await formState(app, formId)).amountB}`);
|
||||
|
||||
// 3b. A brand-new pool has no LP holding yet, so the LP-destination selector shows a fresh
|
||||
// account — no matching holdings, nothing preselected — and the create mints a new LP
|
||||
// account. (create-pool has no lpDefinitionId, so the selector never matches a holding.)
|
||||
const lpSelectorId = await idByObjectName(app, "lpDestinationSelector");
|
||||
await app.waitFor(
|
||||
async () => {
|
||||
if (await prop(app, lpSelectorId, "hasFunds"))
|
||||
throw new Error("unexpected existing LP holding for a brand-new pool");
|
||||
if (await prop(app, lpSelectorId, "selectedAccountId"))
|
||||
throw new Error("LP destination should be unselected (fresh) for create-pool");
|
||||
},
|
||||
{ timeout: 5000, interval: 300, description: "LP destination is a fresh account" },
|
||||
);
|
||||
|
||||
// 4. Submit -> confirmation dialog -> confirm.
|
||||
const dialogId = await idByObjectName(app, "liquidityConfirmDialog");
|
||||
const submitId = await idByObjectName(app, "newPositionSubmitButton");
|
||||
|
||||
@@ -307,6 +307,9 @@ pub(super) fn add_liquidity_quote(request: AddLiquidityQuoteRequest) -> Result<V
|
||||
"expectedLp": delta_lp.to_string(),
|
||||
"minimumLp": minimum_lp.to_string(),
|
||||
"price": price.to_string(),
|
||||
// The pool's LP token definition (base58, matching the holdings' definitionId) so
|
||||
// the UI can offer the wallet's existing LP holdings as the mint destination.
|
||||
"lpDefinitionId": pool.liquidity_pool_id.to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -1036,7 +1036,8 @@ LogosMap AmmModuleImpl::addLiquidityQuote(const LogosMap& request) {
|
||||
if (!quoteResult.ok)
|
||||
return error(quoteResult.error.empty() ? "backend_error" : quoteResult.error);
|
||||
|
||||
// Success: wrap { amountA, amountB, expectedLp, minimumLp, price }.
|
||||
// Success: wrap { amountA, amountB, expectedLp, minimumLp, price, lpDefinitionId }.
|
||||
// (lpDefinitionId is the pool's LP token so the UI can offer existing LP holdings.)
|
||||
LogosMap out = quoteResult.value;
|
||||
out["status"] = "ok";
|
||||
out["error"] = "";
|
||||
|
||||
Reference in New Issue
Block a user