Skip to content

Commit 47cc49e

Browse files
committed
feat(cardano): add full AMM DEX support for Minswap and SundaeSwap
- Implement liquidity operations (add/remove/quote) for both DEXs - Add pool info and swap quote endpoints - Fix token address normalization for Cardano native assets - Support multiple token address formats (dot notation and concatenated) - Add Cardano wallet integration and validation utilities
1 parent cf8a9ae commit 47cc49e

File tree

12 files changed

+174
-141
lines changed

12 files changed

+174
-141
lines changed

src/chains/cardano/cardano.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,15 @@ export class Cardano {
436436
}
437437
}
438438

439+
public async getTokenByAddress(tokenAddress: string): Promise<CardanoToken | undefined> {
440+
const token = this.tokenList.find((token: CardanoToken) => {
441+
const splitAddress = token.address.split('.').join('');
442+
const splitTokenAddress = tokenAddress.split('.').join('');
443+
return splitTokenAddress === splitAddress;
444+
});
445+
return token;
446+
}
447+
439448
async close() {
440449
if (this._chain in Cardano._instances) {
441450
delete Cardano._instances[this._chain];

src/connectors/minswap/amm-routes/addLiquidity.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { TxComplete } from '@aiquant/lucid-cardano';
22
import { calculateDeposit, Dex } from '@aiquant/minswap-sdk';
33
import { FastifyPluginAsync } from 'fastify';
44

5+
import { CardanoToken } from '#src/tokens/types';
6+
57
import {
68
AddLiquidityRequestType,
79
AddLiquidityRequest,
@@ -18,8 +20,8 @@ async function addLiquidity(
1820
network: string,
1921
walletAddress: string,
2022
poolAddress: string,
21-
baseToken: string,
22-
quoteToken: string,
23+
baseToken: CardanoToken,
24+
quoteToken: CardanoToken,
2325
baseTokenAmount: number,
2426
quoteTokenAmount: number,
2527
slippagePct?: number, // decimal, e.g. 0.01 for 1%
@@ -157,8 +159,18 @@ export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => {
157159

158160
const poolInfo = await minswap.getAmmPoolInfo(reqPool);
159161

160-
const baseToken = poolInfo.baseTokenAddress;
161-
const quoteToken = poolInfo.quoteTokenAddress;
162+
const baseTokenAddress = poolInfo.baseTokenAddress;
163+
// console.log('baseTokenAddress', baseTokenAddress);
164+
165+
const quoteTokenAddress = poolInfo.quoteTokenAddress;
166+
// console.log('quoteTokenAddress', quoteTokenAddress);
167+
168+
// Find token symbol from token address
169+
const baseToken = await minswap.cardano.getTokenByAddress(baseTokenAddress);
170+
// console.log('baseToken', baseToken);
171+
172+
const quoteToken = await minswap.cardano.getTokenByAddress(quoteTokenAddress);
173+
// console.log('quoteToken', quoteToken);
162174

163175
return await addLiquidity(
164176
fastify,

src/connectors/minswap/amm-routes/quoteLiquidity.ts

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import { formatTokenAmount } from '../minswap.utils';
1616
export async function getMinswapAmmLiquidityQuote(
1717
network: string,
1818
poolAddress?: string,
19-
baseToken?: string,
20-
quoteToken?: string,
19+
baseToken?: CardanoToken,
20+
quoteToken?: CardanoToken,
2121
baseTokenAmount?: number,
2222
quoteTokenAmount?: number,
2323
_slippagePct?: number,
@@ -45,16 +45,10 @@ export async function getMinswapAmmLiquidityQuote(
4545

4646
const minswap = await Minswap.getInstance(networkToUse);
4747

48-
const baseTokenObj = minswap.cardano.getTokenBySymbol(baseToken);
49-
const quoteTokenObj = minswap.cardano.getTokenBySymbol(quoteToken);
50-
if (!baseTokenObj || !quoteTokenObj) {
51-
throw new Error(`Token not found: ${!baseTokenObj ? baseToken : quoteToken}`);
52-
}
53-
5448
let poolAddressToUse = poolAddress;
5549
let existingPool = true;
5650
if (!poolAddressToUse) {
57-
poolAddressToUse = await minswap.findDefaultPool(baseToken, quoteToken, 'amm');
51+
poolAddressToUse = await minswap.findDefaultPool(baseToken.symbol, quoteToken.symbol, 'amm');
5852
if (!poolAddressToUse) {
5953
existingPool = false;
6054
logger.info(`No existing pool found for ${baseToken}-${quoteToken}, providing theoretical quote`);
@@ -77,11 +71,9 @@ export async function getMinswapAmmLiquidityQuote(
7771
const quoteReserve: bigint = poolState.reserveB;
7872

7973
// ── 3) Convert user inputs into raw bigints ───────────
80-
const baseRaw = baseTokenAmount
81-
? BigInt(Math.floor(baseTokenAmount * 10 ** baseTokenObj.decimals).toString())
82-
: null;
74+
const baseRaw = baseTokenAmount ? BigInt(Math.floor(baseTokenAmount * 10 ** baseToken.decimals).toString()) : null;
8375
const quoteRaw = quoteTokenAmount
84-
? BigInt(Math.floor(quoteTokenAmount * 10 ** quoteTokenObj.decimals).toString())
76+
? BigInt(Math.floor(quoteTokenAmount * 10 ** quoteToken.decimals).toString())
8577
: null;
8678

8779
// ── 4) Compute the “optimal” opposite amount ───────────
@@ -90,21 +82,21 @@ export async function getMinswapAmmLiquidityQuote(
9082
const quoteOptimal = (baseRaw * quoteReserve) / baseReserve;
9183
if (quoteOptimal <= quoteRaw) {
9284
baseLimited = true;
93-
quoteTokenAmountOptimal = Number(formatTokenAmount(quoteOptimal.toString(), quoteTokenObj.decimals));
85+
quoteTokenAmountOptimal = Number(formatTokenAmount(quoteOptimal.toString(), quoteToken.decimals));
9486
} else {
9587
baseLimited = false;
9688
const baseOptimal = (quoteRaw * baseReserve) / quoteReserve;
97-
baseTokenAmountOptimal = Number(formatTokenAmount(baseOptimal.toString(), baseTokenObj.decimals));
89+
baseTokenAmountOptimal = Number(formatTokenAmount(baseOptimal.toString(), baseToken.decimals));
9890
}
9991
} else if (baseRaw !== null) {
10092
// only base provided
10193
const quoteOptimal = baseReserve === BigInt(0) ? BigInt(0) : (baseRaw * quoteReserve) / baseReserve;
102-
quoteTokenAmountOptimal = Number(formatTokenAmount(quoteOptimal.toString(), quoteTokenObj.decimals));
94+
quoteTokenAmountOptimal = Number(formatTokenAmount(quoteOptimal.toString(), quoteToken.decimals));
10395
baseLimited = true;
10496
} else if (quoteRaw !== null) {
10597
// only quote provided
10698
const baseOptimal = quoteReserve === BigInt(0) ? BigInt(0) : (quoteRaw * baseReserve) / quoteReserve;
107-
baseTokenAmountOptimal = Number(formatTokenAmount(baseOptimal.toString(), baseTokenObj.decimals));
99+
baseTokenAmountOptimal = Number(formatTokenAmount(baseOptimal.toString(), baseToken.decimals));
108100
baseLimited = false;
109101
}
110102
} else {
@@ -116,11 +108,9 @@ export async function getMinswapAmmLiquidityQuote(
116108
}
117109

118110
// ── 5) Convert back into Ethers BigNumber for any on‐chain tx ───
119-
const rawBaseTokenAmount = BigNumber.from(
120-
Math.floor(baseTokenAmountOptimal * 10 ** baseTokenObj.decimals).toString(),
121-
);
111+
const rawBaseTokenAmount = BigNumber.from(Math.floor(baseTokenAmountOptimal * 10 ** baseToken.decimals).toString());
122112
const rawQuoteTokenAmount = BigNumber.from(
123-
Math.floor(quoteTokenAmountOptimal * 10 ** quoteTokenObj.decimals).toString(),
113+
Math.floor(quoteTokenAmountOptimal * 10 ** quoteToken.decimals).toString(),
124114
);
125115

126116
return {
@@ -129,8 +119,8 @@ export async function getMinswapAmmLiquidityQuote(
129119
quoteTokenAmount: quoteTokenAmountOptimal,
130120
baseTokenAmountMax: baseTokenAmount ?? baseTokenAmountOptimal,
131121
quoteTokenAmountMax: quoteTokenAmount ?? quoteTokenAmountOptimal,
132-
baseTokenObj,
133-
quoteTokenObj,
122+
baseTokenObj: baseToken,
123+
quoteTokenObj: quoteToken,
134124
poolAddress: poolAddressToUse,
135125
rawBaseTokenAmount,
136126
rawQuoteTokenAmount,
@@ -183,8 +173,11 @@ export const quoteLiquidityRoute: FastifyPluginAsync = async (fastify) => {
183173

184174
const poolInfo = await minswap.getAmmPoolInfo(poolAddress);
185175

186-
const baseToken = poolInfo.baseTokenAddress;
187-
const quoteToken = poolInfo.quoteTokenAddress;
176+
const baseTokenAddress = poolInfo.baseTokenAddress;
177+
const quoteTokenAddress = poolInfo.quoteTokenAddress;
178+
// Find token symbol from token address
179+
const baseToken = await minswap.cardano.getTokenByAddress(baseTokenAddress);
180+
const quoteToken = await minswap.cardano.getTokenByAddress(quoteTokenAddress);
188181

189182
const quote = await getMinswapAmmLiquidityQuote(
190183
network,

src/connectors/minswap/amm-routes/removeLiquidity.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -76,35 +76,33 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => {
7676

7777
const poolInfo = await minswap.getAmmPoolInfo(requestedPoolAddress);
7878

79-
const baseToken = poolInfo.baseTokenAddress;
80-
const quoteToken = poolInfo.quoteTokenAddress;
79+
const baseTokenAddress = poolInfo.baseTokenAddress;
80+
// console.log('baseTokenAddress', baseTokenAddress);
8181

82-
// Resolve tokens
83-
const baseTokenObj = minswap.cardano.getTokenBySymbol(baseToken);
84-
const quoteTokenObj = minswap.cardano.getTokenBySymbol(quoteToken);
82+
const quoteTokenAddress = poolInfo.quoteTokenAddress;
83+
// console.log('quoteTokenAddress', quoteTokenAddress);
8584

86-
if (!baseTokenObj || !quoteTokenObj) {
87-
throw fastify.httpErrors.badRequest(`Token not found: ${!baseTokenObj ? baseToken : quoteToken}`);
85+
// Find token symbol from token address
86+
const baseToken = await minswap.cardano.getTokenByAddress(baseTokenAddress);
87+
// console.log('baseToken', baseToken);
88+
89+
const quoteToken = await minswap.cardano.getTokenByAddress(quoteTokenAddress);
90+
// console.log('quoteToken', quoteToken);
91+
92+
if (!baseToken || !quoteToken) {
93+
throw fastify.httpErrors.badRequest(`Token not found: ${!baseToken ? baseToken : quoteToken}`);
8894
}
8995

9096
// Find pool address if not provided
9197
let poolAddress = requestedPoolAddress;
9298
if (!poolAddress) {
93-
poolAddress = await minswap.findDefaultPool(baseToken, quoteToken, 'amm');
99+
poolAddress = await minswap.findDefaultPool(baseToken.symbol, quoteToken.symbol, 'amm');
94100

95101
if (!poolAddress) {
96102
throw fastify.httpErrors.notFound(`No AMM pool found for pair ${baseToken}-${quoteToken}`);
97103
}
98104
}
99105
// 5) Fetch on-chain pool state for withdraw calculation
100-
const assetA: Asset = {
101-
policyId: baseTokenObj.policyId,
102-
tokenName: baseTokenObj.assetName,
103-
};
104-
const assetB: Asset = {
105-
policyId: quoteTokenObj.policyId,
106-
tokenName: quoteTokenObj.assetName,
107-
};
108106
const { poolState, poolDatum } = await minswap.getPoolData(poolAddress);
109107

110108
// 6) Fetch wallet UTxOs (this also selects the key in Lucid)
@@ -143,8 +141,8 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => {
143141
const fee = txBuild.fee;
144142

145143
// 10) Compute how many tokens were removed (roughly)
146-
const baseTokenAmountRemoved = formatTokenAmount(amountAReceive, baseTokenObj.decimals);
147-
const quoteTokenAmountRemoved = formatTokenAmount(amountBReceive, quoteTokenObj.decimals);
144+
const baseTokenAmountRemoved = formatTokenAmount(amountAReceive, baseToken.decimals);
145+
const quoteTokenAmountRemoved = formatTokenAmount(amountBReceive, quoteToken.decimals);
148146

149147
return {
150148
signature: txHash,

src/connectors/minswap/minswap.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,8 @@ export class Minswap {
171171

172172
return {
173173
address: poolAddress,
174-
baseTokenAddress: pool.assetA,
175-
quoteTokenAddress: pool.assetB,
174+
baseTokenAddress: pool.assetA === 'lovelace' ? 'adalovelace' : pool.assetA,
175+
quoteTokenAddress: pool.assetB === 'lovelace' ? 'adalovelace' : pool.assetB,
176176
feePct: 2,
177177
price: price[0],
178178
baseTokenAmount: baseTokenAmount,

src/connectors/sundaeswap/amm-routes/addLiquidity.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { AssetAmount, IAssetAmountMetadata } from '@sundaeswap/asset';
55
import { BigNumber } from 'ethers';
66
import { FastifyPluginAsync } from 'fastify';
77

8+
import { CardanoToken } from '#src/tokens/types';
9+
810
import {
911
AddLiquidityRequestType,
1012
AddLiquidityRequest,
@@ -22,8 +24,8 @@ async function addLiquidity(
2224
network: string,
2325
walletAddress: string,
2426
poolAddress: string,
25-
baseToken: string,
26-
quoteToken: string,
27+
baseToken: CardanoToken,
28+
quoteToken: CardanoToken,
2729
baseTokenAmount: number,
2830
quoteTokenAmount: number,
2931
slippagePct?: number, // decimal, e.g. 0.01 for 1%
@@ -145,8 +147,12 @@ export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => {
145147

146148
const poolInfo = await sundaeswap.getAmmPoolInfo(reqPool);
147149

148-
const baseToken = poolInfo.baseTokenAddress;
149-
const quoteToken = poolInfo.quoteTokenAddress;
150+
const baseTokenAddress = poolInfo.baseTokenAddress;
151+
const quoteTokenAddress = poolInfo.quoteTokenAddress;
152+
// Find token symbol from token address
153+
const baseToken = await sundaeswap.cardano.getTokenByAddress(baseTokenAddress);
154+
const quoteToken = await sundaeswap.cardano.getTokenByAddress(quoteTokenAddress);
155+
150156
return await addLiquidity(
151157
fastify,
152158
network || 'mainnet',

src/connectors/sundaeswap/amm-routes/poolInfo.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const ammPoolInfoRoute: FastifyPluginAsync = async (fastify) => {
1818
...GetPoolInfoRequest,
1919
properties: {
2020
network: { type: 'string', examples: ['mainnet'] },
21-
poolIdent: {
21+
poolAddress: {
2222
type: 'string',
2323
examples: ['2f36866691fa75a9aab66dec99f7cc2d297ca09e34d9ce68cde04773'],
2424
},
@@ -33,18 +33,18 @@ export const ammPoolInfoRoute: FastifyPluginAsync = async (fastify) => {
3333
},
3434
async (request): Promise<PoolInfo> => {
3535
try {
36-
const { poolAddress: poolIdent } = request.query;
36+
const { poolAddress } = request.query;
3737
const network = request.query.network || 'mainnet';
3838

3939
const sundaeswap = await Sundaeswap.getInstance(network);
40+
// console.log('sundaeswap ', sundaeswap);
4041

41-
// Check if either poolIdent or both baseToken and quoteToken are provided
42-
if (!poolIdent) {
43-
throw fastify.httpErrors.badRequest('poolIdent is required');
42+
// Check if either poolAddress or both baseToken and quoteToken are provided
43+
if (!poolAddress) {
44+
throw fastify.httpErrors.badRequest('poolAddress is required');
4445
}
4546

46-
const poolIdentToUse = poolIdent;
47-
const poolInfo = await sundaeswap.getAmmPoolInfo(poolIdentToUse);
47+
const poolInfo = await sundaeswap.getAmmPoolInfo(poolAddress);
4848
if (!poolInfo) throw fastify.httpErrors.notFound('Pool not found');
4949
return poolInfo;
5050
} catch (e) {

0 commit comments

Comments
 (0)