Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,21 @@
"packageManager": "pnpm@10.18.1",
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/json-bigint": "^1.0.4",
"@types/node": "^24.10.1",
"eslint": "^9.39.1",
"json-bigint": "^1.0.0",
"prettier": "^3.6.2",
"tsup": "^8.5.1",
"tsx": "^4.20.6",
"typescript": "^5.9.3",
"typescript-eslint": "^8.48.0",
"vitest": "^4.0.13"
},
"dependencies": {
"@anastasia-labs/cardano-multiplatform-lib-nodejs": "6.0.2-3",
"dotenv": "^17.2.3",
"@lucid-evolution/lucid": "^0.4.29",
"dotenv": "^17.2.3",
"winston": "^3.18.3"
}
}
64 changes: 53 additions & 11 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions src/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Blockfrost, Lucid } from '@lucid-evolution/lucid';
import { updatePoolFeeTx } from './tx';

async function main() {
const blockfrostProjectId = '<>YOUR_BLOCKFROST_PROJECT_ID<>';
const blockfrostUrl = 'https://cardano-preprod.blockfrost.io/api/v0';

// Example wallet: It has been granted permissions to manage the pool
const seed =
'mouse carpet zebra giant just pizza very song simple state rebel lunar above naive bundle accuse buffalo hurry mango scorpion country silly layer average';
Comment on lines +9 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this out of source control into a gitignored .env

const walletAddr =
'addr_test1qz280luwccrquk3lk4ykjjsexl840ppsy2u6vtq4wflwa2c279l7xk8kwpt4zc27c4dsngvpyuqn700m3s8e0k6fw9nsdudnqx';
Comment on lines +11 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be derived from the seed. I.e. after lucid initialisation from the seed, get the address from the lucid instance.

// The LP asset for the pool we want to update fees for
const poolLPAsset = {
policyId: 'd6aae2059baee188f74917493cf7637e679cd219bdfbbf4dcbeb1d0b',
tokenName:
'0c9dfded2857d530ce2cc536d4517a6e4c54e280f5e89b4f315fe0d81cabbcf5',
};
Comment on lines +14 to +18
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's pass only the token name to the tx building, and let the tx building pick the right LP policy from the constant there.


const lucid = await Lucid(
new Blockfrost(blockfrostUrl, blockfrostProjectId),
'Preprod',
);

lucid.selectWallet.fromSeed(seed);

const tx = updatePoolFeeTx(lucid, {
managerAddress: walletAddr,
poolLPAsset: poolLPAsset,
newFeeA: 0.9,
newFeeB: 0.3,
Comment on lines +30 to +31
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's maybe rename these parameters to newFeeAPercent and newFeeBPercent so it's semantically clear. AFAIK these parameters set the fee for A and B to 0.9% and 0.3% respectively.

});

const txComplete = await tx.complete();
const signedTx = await txComplete.sign.withWallet().complete();
const txHash = await signedTx.submit();
console.log('Transaction submitted successfully: ', txHash);
}

void main();
110 changes: 110 additions & 0 deletions src/tx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { getAddressDetails, LucidEvolution, TxBuilder } from "@lucid-evolution/lucid";
import JSONBig from "json-bigint"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aaa, this is nice, didn't know about this package, thanks.


const MIN_TRADING_FEE: bigint = 5n; // 0.05%
const MAX_TRADING_FEE: bigint = 2000n; // 20%

const LP_POLICY_ID_CFG = {
["Mainnet"]: "f5808c2c990d86da54bfc97d89cee6efa20cd8461616359478d96b4c",
["Preprod"]: "d6aae2059baee188f74917493cf7637e679cd219bdfbbf4dcbeb1d0b",
}

function assetToDottedString(asset: { policyId: string; tokenName: string }): string {
if (asset.policyId === "" && asset.tokenName === "") {
return "lovelace"
}
if (asset.tokenName === "") {
return asset.policyId;
}
return `${asset.policyId}.${asset.tokenName}`;
}
Comment on lines +12 to +20
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should utilise the toUnit from the @lucid-evolution package


/**
* Request to update the trading fees for a liquidity pool.
* @property managerAddress - The address of the pool manager authorized to update fees
* @property poolLPAsset - The LP token asset identifying the pool
* @property newFeeA - The new fee for trading direction A as a percentage (0.05% - 20%)
* @property newFeeB - The new fee for trading direction B as a percentage (0.05% - 20%)
* @property version - Protocol version for the fee request format
*/
export type PoolFeeRequest = {
managerAddress: string;
poolLPAsset: {
policyId: string,
tokenName: string
};
Comment on lines +32 to +35
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above, this should be only a token name.

newFeeA: number;
newFeeB: number;
version: "1";
};

export type RequestPoolFeeOptions = {
request: Omit<PoolFeeRequest, "version">;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious what the reason is for omitting the version field?

};

/**
* Creates a transaction to update the trading fees for a liquidity pool.
* This method builds a transaction with metadata that requests fee changes for a pool.
* The transaction must be signed by the pool manager address.
*/
export function updatePoolFeeTx(
lucid: LucidEvolution,
options: Omit<PoolFeeRequest, "version">
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the RequestPoolFeeOptions type here instead.

): TxBuilder {
const { managerAddress, poolLPAsset, newFeeA, newFeeB } = options;
const newFeeABps = BigInt(Math.floor(newFeeA * 100));
const newFeeBBps = BigInt(Math.floor(newFeeB * 100));

if (newFeeABps < MIN_TRADING_FEE || newFeeABps > MAX_TRADING_FEE) {
throw new Error(
`Liquidity Pool Fee A must be in 0.05% - 20%, actual: ${newFeeA}%`
);
}
if (newFeeBBps < MIN_TRADING_FEE || newFeeBBps > MAX_TRADING_FEE) {
throw new Error(
`Liquidity Pool Fee B must be in 0.05% - 20%, actual: ${newFeeB}%`
Comment on lines +60 to +65
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, use the MIN_TRADING_FEE and MAX_TRADING_FEE constants in both of those error logs.

);
}

const network = lucid.config().network;
if (network !== "Mainnet" && network !== "Preprod") {
throw new Error(`Unsupported network: ${network}`);
}

if (poolLPAsset.policyId != LP_POLICY_ID_CFG[network]) {
throw new Error(
`Invalid LP Policy ID for network ${network}: ${poolLPAsset.policyId}`
);
}

const feeRequestJSON = JSONBig.stringify({
managerAddress: managerAddress,
poolLPAsset: assetToDottedString(poolLPAsset),
newFeeA: newFeeABps.toString(),
newFeeB: newFeeBBps.toString(),
version: "1",
}).match(/.{1,64}/g);

if (!feeRequestJSON) {
throw new Error("Failed to create fee request JSON metadata");
}
const addressDetails = getAddressDetails(managerAddress);
if (addressDetails.type != "Base" && addressDetails.type != "Enterprise") {
throw new Error("Manager address must be a Base or Enterprise address");
}
const paymentCred = addressDetails.paymentCredential;
if (!paymentCred) {
throw new Error("Manager address must have a payment credential");
}
if (paymentCred.type !== "Key") {
throw new Error("Manager address must be a key address");
}

return lucid
.newTx()
.addSigner(managerAddress)
.attachMetadata(674, {
["msg"]: ["Minswap: Request of Pool Fee Manager"],
["extraData"]: feeRequestJSON,
});
Comment on lines +103 to +109
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tx building still missing here.

}
Loading