Skip to content

Commit

Permalink
implement Pool V2 creation transaction (#32)
Browse files Browse the repository at this point in the history
* implement Pool V2 creation transaction

* rename
  • Loading branch information
h2physics authored Jul 25, 2024
1 parent a93a786 commit 44e1057
Show file tree
Hide file tree
Showing 10 changed files with 437 additions and 3 deletions.
20 changes: 20 additions & 0 deletions examples/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
calculateWithdraw,
calculateZapIn,
Dex,
DexV2,
NetworkId,
PoolV1,
} from "../src";
Expand Down Expand Up @@ -349,6 +350,25 @@ async function _cancelTxExample(
});
}

async function _createPoolV2(
lucid: Lucid,
blockFrostAdapter: BlockfrostAdapter
): Promise<TxComplete> {
const dexV2 = new DexV2(lucid, blockFrostAdapter);
const txComplete = await dexV2.createPoolTx({
assetA: ADA,
assetB: {
policyId: "e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72",
tokenName: "434d",
},
amountA: 10_000000n,
amountB: 300_000000n,
tradingFeeNumerator: 100n,
});

return txComplete;
}

/**
* Initialize Lucid Instance for Browser Environment
* @param network Network you're working on
Expand Down
57 changes: 57 additions & 0 deletions src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import {
DexV2Constant,
StableswapConstant,
} from "./types/constants";
import { FactoryV2 } from "./types/factory";
import { NetworkId } from "./types/network";
import { PoolV1, PoolV2, StablePool } from "./types/pool";
import {
checkValidPoolOutput,
isValidPoolOutput,
normalizeAssets,
} from "./types/pool.internal";
import { StringUtils } from "./types/string";
import { TxHistory } from "./types/tx.internal";
import { getScriptHashFromAddress } from "./utils/address-utils.internal";

Expand Down Expand Up @@ -393,4 +395,59 @@ export class BlockfrostAdapter {

return null;
}

public async getAllFactoriesV2(): Promise<{
factories: FactoryV2.State[];
errors: unknown[];
}> {
const v2Config = DexV2Constant.CONFIG[this.networkId];
const utxos = await this.api.addressesUtxosAssetAll(
v2Config.factoryScriptHashBech32,
v2Config.factoryAsset
);

const factories: FactoryV2.State[] = [];
const errors: unknown[] = [];
for (const utxo of utxos) {
try {
if (!utxo.inline_datum) {
throw new Error(
`Cannot find datum of Factory V2, tx: ${utxo.tx_hash}`
);
}
const factory = new FactoryV2.State(
this.networkId,
utxo.address,
{ txHash: utxo.tx_hash, index: utxo.output_index },
utxo.amount,
utxo.inline_datum
);
factories.push(factory);
} catch (err) {
errors.push(err);
}
}
return {
factories: factories,
errors: errors,
};
}

public async getFactoryV2ByPair(
assetA: Asset,
assetB: Asset
): Promise<FactoryV2.State | null> {
const factoryIdent = PoolV2.computeLPAssetName(assetA, assetB);
const { factories: allFactories } = await this.getAllFactoriesV2();
for (const factory of allFactories) {
if (
StringUtils.compare(factory.head, factoryIdent) < 0 &&
StringUtils.compare(factoryIdent, factory.tail) < 0
) {
return factory;
}
}

return null;
}
}
22 changes: 22 additions & 0 deletions src/calculate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,25 @@ export function calculateZapIn(options: CalculateZapInOptions): bigint {
(reserveOut - swapToAssetOutAmount)
);
}

export namespace DexV2Calculation {
export const MAX_LIQUIDITY = 9_223_372_036_854_775_807n;
export const DEFAULT_POOL_ADA = 4_500_000n;
// The amount of liquidity that will be locked in pool when creating pools
export const MINIMUM_LIQUIDITY = 10n;

export type InitialLiquidityOptions = {
amountA: bigint;
amountB: bigint;
};
export function calculateInitialLiquidity({
amountA,
amountB,
}: InitialLiquidityOptions): bigint {
let x = sqrt(amountA * amountB);
if (x * x < amountA * amountB) {
x += 1n;
}
return x;
}
}
209 changes: 209 additions & 0 deletions src/dex-v2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import invariant from "@minswap/tiny-invariant";
import {
Assets,
Constr,
Data,
getAddressDetails,
Lucid,
TxComplete,
} from "lucid-cardano";

import {
Asset,
BlockfrostAdapter,
DexV2Calculation,
DexV2Constant,
MetadataMessage,
PoolV2,
} from ".";
import { FactoryV2 } from "./types/factory";
import { NetworkId } from "./types/network";

/**
* Options for building Pool V2 Creation transaction
* @assetA
* @assetB
* @amountA
* @amountB
* @tradingFeeNumerator numerator of Pool's trading fee with denominator 10000
* Eg:
* - fee 0.05% -> tradingFeeNumerator 5
* - fee 0.3% -> tradingFeeNumerator 30
* - fee 1% -> tradingFeeNumerator 100
*/
export type CreatePoolV2Options = {
assetA: Asset;
assetB: Asset;
amountA: bigint;
amountB: bigint;
tradingFeeNumerator: bigint;
};

export class DexV2 {
private readonly lucid: Lucid;
private readonly networkId: NetworkId;
private readonly adapter: BlockfrostAdapter;

constructor(lucid: Lucid, adapter: BlockfrostAdapter) {
this.lucid = lucid;
this.networkId =
lucid.network === "Mainnet" ? NetworkId.MAINNET : NetworkId.TESTNET;
this.adapter = adapter;
}

async createPoolTx({
assetA,
assetB,
amountA,
amountB,
tradingFeeNumerator,
}: CreatePoolV2Options): Promise<TxComplete> {
const config = DexV2Constant.CONFIG[this.networkId];
// Sort ascendingly assets and its amount
const [sortedAssetA, sortedAssetB, sortedAmountA, sortedAmountB] =
Asset.compare(assetA, assetB) < 0
? [assetA, assetB, amountA, amountB]
: [assetB, assetA, amountB, amountA];

const factory = await this.adapter.getFactoryV2ByPair(
sortedAssetA,
sortedAssetB
);
invariant(
factory,
`cannot find available Factory V2 Utxo, the liquidity pool might be created before`
);

const initialLiquidity = DexV2Calculation.calculateInitialLiquidity({
amountA: sortedAmountA,
amountB: sortedAmountB,
});
const remainingLiquidity =
DexV2Calculation.MAX_LIQUIDITY -
(initialLiquidity - DexV2Calculation.MINIMUM_LIQUIDITY);
const lpAssetName = PoolV2.computeLPAssetName(sortedAssetA, sortedAssetB);
const lpAsset: Asset = {
policyId: config.lpPolicyId,
tokenName: lpAssetName,
};
const poolBatchingStakeCredential = getAddressDetails(
config.poolBatchingAddress
)?.stakeCredential;
invariant(
poolBatchingStakeCredential,
`cannot parse Liquidity Pool batching address`
);
const poolDatum: PoolV2.Datum = {
poolBatchingStakeCredential: poolBatchingStakeCredential,
assetA: sortedAssetA,
assetB: sortedAssetB,
totalLiquidity: initialLiquidity,
reserveA: sortedAmountA,
reserveB: sortedAmountB,
baseFee: {
feeANumerator: tradingFeeNumerator,
feeBNumerator: tradingFeeNumerator,
},
feeSharingNumerator: undefined,
allowDynamicFee: false,
};

const poolValue: Assets = {
lovelace: DexV2Calculation.DEFAULT_POOL_ADA,
[Asset.toString(lpAsset)]: remainingLiquidity,
[config.poolAuthenAsset]: 1n,
};
if (poolValue[Asset.toString(sortedAssetA)]) {
poolValue[Asset.toString(sortedAssetA)] += sortedAmountA;
} else {
poolValue[Asset.toString(sortedAssetA)] = sortedAmountA;
}
if (poolValue[Asset.toString(sortedAssetB)]) {
poolValue[Asset.toString(sortedAssetB)] += sortedAmountB;
} else {
poolValue[Asset.toString(sortedAssetB)] = sortedAmountB;
}

const deployedScripts = DexV2Constant.DEPLOYED_SCRIPTS[this.networkId];

const factoryRefs = await this.lucid.utxosByOutRef([
deployedScripts.factory,
]);
invariant(
factoryRefs.length === 1,
"cannot find deployed script for Factory Validator"
);
const factoryRef = factoryRefs[0];
const authenRefs = await this.lucid.utxosByOutRef([deployedScripts.authen]);
invariant(
authenRefs.length === 1,
"cannot find deployed script for Authen Minting Policy"
);
const authenRef = authenRefs[0];
const factoryUtxos = await this.lucid.utxosByOutRef([
{
txHash: factory.txIn.txHash,
outputIndex: factory.txIn.index,
},
]);
invariant(factoryUtxos.length === 1, "cannot find Utxo of Factory");
const factoryUtxo = factoryUtxos[0];

const factoryRedeemer: FactoryV2.Redeemer = {
assetA: sortedAssetA,
assetB: sortedAssetB,
};

const newFactoryDatum1: FactoryV2.Datum = {
head: factory.head,
tail: lpAssetName,
};
const newFactoryDatum2: FactoryV2.Datum = {
head: lpAssetName,
tail: factory.tail,
};

return this.lucid
.newTx()
.readFrom([factoryRef, authenRef])
.collectFrom(
[factoryUtxo],
Data.to(FactoryV2.Redeemer.toPlutusData(factoryRedeemer))
)
.payToContract(
config.poolCreationAddress,
{
inline: Data.to(PoolV2.Datum.toPlutusData(poolDatum)),
},
poolValue
)
.payToContract(
config.factoryAddress,
{
inline: Data.to(FactoryV2.Datum.toPlutusData(newFactoryDatum1)),
},
{
[config.factoryAsset]: 1n,
}
)
.payToContract(
config.factoryAddress,
{
inline: Data.to(FactoryV2.Datum.toPlutusData(newFactoryDatum2)),
},
{
[config.factoryAsset]: 1n,
}
)
.mintAssets(
{
[Asset.toString(lpAsset)]: DexV2Calculation.MAX_LIQUIDITY,
[config.factoryAsset]: 1n,
[config.poolAuthenAsset]: 1n,
},
Data.to(new Constr(1, []))
)
.attachMetadata(674, { msg: [MetadataMessage.CREATE_POOL] })
.complete();
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from "./adapter";
export * from "./calculate";
export * from "./dex";
export * from "./dex-v2";
export * from "./types/asset";
export * from "./types/constants";
export * from "./types/network";
Expand Down
9 changes: 9 additions & 0 deletions src/types/asset.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Constr, Data } from "lucid-cardano";

import { StringUtils } from "./string";

export const ADA: Asset = {
policyId: "",
tokenName: ""
Expand Down Expand Up @@ -51,4 +53,11 @@ export namespace Asset {
tokenName: data.fields[1] as string
}
}

export function compare(a1: Asset, a2: Asset): number {
if (a1.policyId === a2.policyId) {
return StringUtils.compare(a1.tokenName, a2.tokenName)
}
return StringUtils.compare(a1.policyId, a2.policyId)
}
}
Loading

0 comments on commit 44e1057

Please sign in to comment.