Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MIN-1548: Auto cancel expired orders worker #47

Merged
merged 11 commits into from
Nov 27, 2024
91 changes: 68 additions & 23 deletions docs/transaction.md → docs/dex-transaction.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Minswap AMM V2 & Stableswap Classes Documentation
# Minswap DEX Classes documentation

## Overview

Expand All @@ -9,6 +9,8 @@ This documentation provides details on how to interact with the **Stableswap** a
- **Stableswap class**: Located in `src/stableswap.ts`.
- **AMM V2 class**: Located in `src/dex-v2.ts`.
- **Example file**: Demonstrates usage of both classes, located in `examples/example.ts`.
- **DexV2Worker Class**: Located in `src/dex-v2-worker.ts`.
- **DexV2Worker Example**: Located in `examples/dex-v2-worker-example.ts`.

### Utility Functions

Expand Down Expand Up @@ -54,7 +56,10 @@ const blockfrostAdapter = new BlockfrostAdapter(
const utxos = await lucid.utxosAt(address);

const lpAsset = Asset.fromString("<STABLE_POOL_LP_ASSET>");
const config = StableswapConstant.getConfigByLpAsset(lpAsset, NetworkId.TESTNET);
const config = StableswapConstant.getConfigByLpAsset(
lpAsset,
NetworkId.TESTNET
);

const pool = await blockfrostAdapter.getStablePoolByLpAsset(lpAsset);

Expand Down Expand Up @@ -92,7 +97,9 @@ const txComplete = await new Stableswap(lucid).createBulkOrdersTx({
],
});

const signedTx = await txComplete.signWithPrivateKey("<YOUR_PRIVATE_KEY>").complete();
const signedTx = await txComplete
.signWithPrivateKey("<YOUR_PRIVATE_KEY>")
.complete();
const txId = await signedTx.submit();
console.info(`Transaction submitted successfully: ${txId}`);
```
Expand Down Expand Up @@ -144,27 +151,30 @@ const acceptedAmountOut = Slippage.apply({
type: "down",
});

const txComplete = await new DexV2(lucid, blockfrostAdapter).createBulkOrdersTx({
sender: address,
availableUtxos: utxos,
orderOptions: [
{
type: OrderV2.StepType.SWAP_EXACT_IN,
amountIn: swapAmount,
assetIn: assetA,
direction: OrderV2.Direction.A_TO_B,
minimumAmountOut: acceptedAmountOut,
lpAsset: pool.lpAsset,
isLimitOrder: false,
killOnFailed: false,
},
],
});
const txComplete = await new DexV2(lucid, blockfrostAdapter).createBulkOrdersTx(
{
sender: address,
availableUtxos: utxos,
orderOptions: [
{
type: OrderV2.StepType.SWAP_EXACT_IN,
amountIn: swapAmount,
assetIn: assetA,
direction: OrderV2.Direction.A_TO_B,
minimumAmountOut: acceptedAmountOut,
lpAsset: pool.lpAsset,
isLimitOrder: false,
killOnFailed: false,
},
],
}
);

const signedTx = await txComplete.signWithPrivateKey("<YOUR_PRIVATE_KEY>").complete();
const signedTx = await txComplete
.signWithPrivateKey("<YOUR_PRIVATE_KEY>")
.complete();
const txId = await signedTx.submit();
console.info(`Transaction submitted successfully: ${txId}`);

```

### 3. Create the DEX V2 Liquiditiy Pool
Expand Down Expand Up @@ -204,19 +214,54 @@ const txComplete = await new DexV2(lucid, blockfrostAdapter).createPoolTx({
tradingFeeNumerator: 100n,
});

const signedTx = await txComplete.signWithPrivateKey("<YOUR_PRIVATE_KEY>").complete();
const signedTx = await txComplete
.signWithPrivateKey("<YOUR_PRIVATE_KEY>")
.complete();
const txId = await signedTx.submit();
console.info(`Transaction submitted successfully: ${txId}`);
```

### 4. Run Dex V2 Worker
m1n999999 marked this conversation as resolved.
Show resolved Hide resolved

```ts
const network: Network = "Preprod";
const blockfrostProjectId = "<YOUR_BLOCKFROST_API_KEY>";
const blockfrostUrl = "https://cardano-preprod.blockfrost.io/api/v0";

const address = "<YOUR_ADDRESS>";
const lucid = await getBackendLucidInstance(
network,
blockfrostProjectId,
blockfrostUrl,
address
);

const blockfrostAdapter = new BlockfrostAdapter(
NetworkId.TESTNET,
new BlockFrostAPI({
projectId: blockfrostProjectId,
network: "preprod",
})
);

const worker = new DexV2Worker({
lucid,
blockfrostAdapter,
privateKey: "<YOUR_PRIVATE_KEY>",
});

await worker.start();
```

## Additional Examples

You can explore more examples in the [Examples](../examples/example.ts) folder to learn how to integrate the Stableswap and DexV2 classes in more complex scenarios.

## Conclusion

The Stableswap and AMM V2 classes offer powerful tools for interacting with Minswap’s decentralized exchange. They allow users to easily manage liquidity pools and make swaps, with built-in support for Minswap Batcher Fee discounts. By utilizing these classes, users can create efficient transactions and leverage the utility of $MIN to reduce costs.

For more details, you can refer to the specific class files:

- [Stableswap class](../src/stableswap.ts)
- [AMM V2 class](../src/dex-v2.ts)
- [AMM V2 class](../src/dex-v2.ts)
39 changes: 39 additions & 0 deletions examples/dex-v2-worker-example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { BlockFrostAPI } from "@blockfrost/blockfrost-js";
import { Network } from "@minswap/lucid-cardano";

import { BlockfrostAdapter, NetworkId } from "../src";
import { DexV2Worker } from "../src/dex-v2-worker";
import { getBackendLucidInstance } from "../src/utils/lucid";

async function main(): Promise<void> {
const network: Network = "Preprod";
const blockfrostProjectId = "<YOUR_BLOCKFROST_API_KEY>";
const blockfrostUrl = "https://cardano-preprod.blockfrost.io/api/v0";

const address =
"addr_test1qqf2dhk96l2kq4xh2fkhwksv0h49vy9exw383eshppn863jereuqgh2zwxsedytve5gp9any9jwc5hz98sd47rwfv40stc26fr";
const lucid = await getBackendLucidInstance(
network,
blockfrostProjectId,
blockfrostUrl,
address
);

const blockfrostAdapter = new BlockfrostAdapter(
NetworkId.TESTNET,
new BlockFrostAPI({
projectId: blockfrostProjectId,
network: "preprod",
})
);

const worker = new DexV2Worker({
lucid,
blockfrostAdapter,
privateKey: "<YOUR_PRIVATE_KEY>",
});

await worker.start();
}

void main();
54 changes: 54 additions & 0 deletions src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
import { FactoryV2 } from "./types/factory";
import { LbeV2Types } from "./types/lbe-v2";
import { NetworkEnvironment, NetworkId } from "./types/network";
import { OrderV2 } from "./types/order";
import { PoolV1, PoolV2, StablePool } from "./types/pool";
import {
checkValidPoolOutput,
Expand Down Expand Up @@ -272,6 +273,7 @@ export class BlockfrostAdapter implements Adapter {
return latestBlock.slot ?? 0;
}

// MARK: DEX V1
public async getV1PoolInTx({
txHash,
}: GetPoolInTxParams): Promise<PoolV1.State | null> {
Expand Down Expand Up @@ -386,6 +388,7 @@ export class BlockfrostAdapter implements Adapter {
return [priceAB, priceBA];
}

// MARK: DEX V2
public async getAllV2Pools(): Promise<{
pools: PoolV2.State[];
errors: unknown[];
Expand Down Expand Up @@ -569,6 +572,57 @@ export class BlockfrostAdapter implements Adapter {
return null;
}

public async getAllV2Orders(): Promise<{
orders: OrderV2.State[];
errors: unknown[];
}> {
const v2Config = DexV2Constant.CONFIG[this.networkId];
const utxos = await this.blockFrostApi.addressesUtxosAll(
v2Config.orderScriptHashBech32
);

const orders: OrderV2.State[] = [];
const errors: unknown[] = [];
for (const utxo of utxos) {
try {
let order: OrderV2.State | undefined = undefined;
if (utxo.inline_datum !== null) {
order = new OrderV2.State(
this.networkId,
utxo.address,
{ txHash: utxo.tx_hash, index: utxo.output_index },
utxo.amount,
utxo.inline_datum
);
} else if (utxo.data_hash !== null) {
const orderDatum = await this.blockFrostApi.scriptsDatumCbor(
utxo.data_hash
);
order = new OrderV2.State(
this.networkId,
utxo.address,
{ txHash: utxo.tx_hash, index: utxo.output_index },
utxo.amount,
orderDatum.cbor
);
}

if (order === undefined) {
throw new Error(`Cannot find datum of Order V2, tx: ${utxo.tx_hash}`);
}

orders.push(order);
} catch (err) {
errors.push(err);
}
}
return {
orders: orders,
errors: errors,
};
}

// MARK: STABLESWAP
private async parseStablePoolState(
utxo: Responses["address_utxo_content"][0]
): Promise<StablePool.State> {
Expand Down
116 changes: 116 additions & 0 deletions src/dex-v2-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Lucid } from "@minswap/lucid-cardano";

import { BlockfrostAdapter, DexV2, DexV2Constant, OrderV2 } from ".";
import { runRecurringJob } from "./utils/job";

type DexV2WorkerConstructor = {
lucid: Lucid;
blockfrostAdapter: BlockfrostAdapter;
privateKey: string;
};

export class DexV2Worker {
m1n999999 marked this conversation as resolved.
Show resolved Hide resolved
private readonly lucid: Lucid;
private readonly blockfrostAdapter: BlockfrostAdapter;
private readonly privateKey: string;

constructor({
lucid,
blockfrostAdapter,
privateKey,
}: DexV2WorkerConstructor) {
this.lucid = lucid;
this.blockfrostAdapter = blockfrostAdapter;
this.privateKey = privateKey;
}

async start(): Promise<void> {
await runRecurringJob({
name: "lbe v2 batcher",
interval: 1000 * 30, // 30s
job: () => this.runWorker(),
});
}

async runWorker(): Promise<void> {
console.info("start run dex v2 worker");
const { orders: allOrders } = await this.blockfrostAdapter.getAllV2Orders();
const currentSlot = await this.blockfrostAdapter.currentSlot();
const currentTime = this.lucid.utils.slotToUnixTime(currentSlot);
const mapDatum: Record<string, string> = {};
const orders: OrderV2.State[] = [];
for (const order of allOrders) {
const orderDatum = order.datum;
const expiredOptions = orderDatum.expiredOptions;
if (expiredOptions === undefined) {
continue;
}
if (expiredOptions.expiredTime >= BigInt(currentTime)) {
continue;
}
if (
expiredOptions.maxCancellationTip < DexV2Constant.DEFAULT_CANCEL_TIPS
) {
continue;
}
const receiverDatum = orderDatum.refundReceiverDatum;
let rawDatum: string | undefined = undefined;
if (
receiverDatum.type === OrderV2.ExtraDatumType.INLINE_DATUM ||
receiverDatum.type === OrderV2.ExtraDatumType.DATUM_HASH
) {
try {
rawDatum = await this.blockfrostAdapter.getDatumByDatumHash(
receiverDatum.hash
);
mapDatum[receiverDatum.hash] = rawDatum;
// eslint-disable-next-line unused-imports/no-unused-vars
} catch (_err) {
continue;
}
}
orders.push(order);

// CANCEL MAX 20 Orders
if (orders.length === 20) {
break;
}
}

if (orders.length === 0) {
console.info(`SKIP | No orders.`);
return;
}
const orderUtxos = await this.lucid.utxosByOutRef(
orders.map((order) => ({
txHash: order.txIn.txHash,
outputIndex: order.txIn.index,
}))
);
if (orderUtxos.length === 0) {
console.info(`SKIP | Can not find any order utxos.`);
return;
}
try {
const txComplete = await new DexV2(
this.lucid,
this.blockfrostAdapter
).cancelExpiredOrders({
orderUtxos: orderUtxos,
currentSlot,
extraDatumMap: mapDatum,
});
const signedTx = await txComplete
.signWithPrivateKey(this.privateKey)
.complete();

const txId = await signedTx.submit();
console.info(`Transaction submitted successfully: ${txId}`);
} catch (_err) {
console.error(
`Error when the worker runs: orders ${orders.map((order) => `${order.txIn.txHash}#${order.txIn.index}`).join(", ")}`,
_err
);
}
}
}
Loading