From 8185861e1a1cd4ffb8fb1476d6c2ca185f1fae61 Mon Sep 17 00:00:00 2001 From: Ha Quang Minh Date: Tue, 26 Nov 2024 13:50:56 +0700 Subject: [PATCH 1/9] init dex v2 worker --- examples/dex-v2-worker-example.ts | 43 +++++++++++ examples/example.ts | 2 +- examples/lbe-v2-worker-example.ts | 2 +- src/adapter.ts | 54 +++++++++++++ src/dex-v2-worker.ts | 107 ++++++++++++++++++++++++++ src/dex-v2.ts | 123 ++++++++++++++++++++++++++---- src/types/constants.ts | 39 +++++++--- src/types/order.ts | 23 ++++++ 8 files changed, 367 insertions(+), 26 deletions(-) create mode 100644 examples/dex-v2-worker-example.ts create mode 100644 src/dex-v2-worker.ts diff --git a/examples/dex-v2-worker-example.ts b/examples/dex-v2-worker-example.ts new file mode 100644 index 0000000..c06ff5e --- /dev/null +++ b/examples/dex-v2-worker-example.ts @@ -0,0 +1,43 @@ +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 { NetworkEnvironment } from "../src/types/network"; +import { getBackendLucidInstance } from "../src/utils/lucid"; + +async function main(): Promise { + const network: Network = "Preprod"; + const blockfrostProjectId = "preprodSj4PM4LDOTa2BbfAY4XIEqASI9gKzOEz"; + 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({ + networkEnv: NetworkEnvironment.TESTNET_PREPROD, + networkId: NetworkId.TESTNET, + lucid, + blockfrostAdapter, + privateKey: + "ed25519e_sk1pqs6ssazw755demuks2974mdwu6stz0uxpj543edm5cm0y96p9gk9lcv5jspdg3aq7wtv9r96uaru0rnu4qdm7lccarntjm22mtk72cm5cjrj", + }); + + await worker.start(); +} + +void main(); diff --git a/examples/example.ts b/examples/example.ts index f651b52..e55a02f 100644 --- a/examples/example.ts +++ b/examples/example.ts @@ -36,8 +36,8 @@ import { import { LbeV2 } from "../src/lbe-v2/lbe-v2"; import { Stableswap } from "../src/stableswap"; import { LbeV2Types } from "../src/types/lbe-v2"; -import { Slippage } from "../src/utils/slippage.internal"; import { getBackendLucidInstance } from "../src/utils/lucid"; +import { Slippage } from "../src/utils/slippage.internal"; const MIN: Asset = { policyId: "e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72", diff --git a/examples/lbe-v2-worker-example.ts b/examples/lbe-v2-worker-example.ts index 0ba294d..28d20b0 100644 --- a/examples/lbe-v2-worker-example.ts +++ b/examples/lbe-v2-worker-example.ts @@ -2,8 +2,8 @@ import { BlockFrostAPI } from "@blockfrost/blockfrost-js"; import { Network } from "@minswap/lucid-cardano"; import { BlockfrostAdapter, NetworkId } from "../src"; -import { NetworkEnvironment } from "../src/types/network"; import { LbeV2Worker } from "../src/lbe-v2-worker/worker"; +import { NetworkEnvironment } from "../src/types/network"; import { getBackendLucidInstance } from "../src/utils/lucid"; async function main(): Promise { diff --git a/src/adapter.ts b/src/adapter.ts index 57f715f..e003aa1 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -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, @@ -272,6 +273,7 @@ export class BlockfrostAdapter implements Adapter { return latestBlock.slot ?? 0; } + // MARK: DEX V1 public async getV1PoolInTx({ txHash, }: GetPoolInTxParams): Promise { @@ -386,6 +388,7 @@ export class BlockfrostAdapter implements Adapter { return [priceAB, priceBA]; } + // MARK: DEX V2 public async getAllV2Pools(): Promise<{ pools: PoolV2.State[]; errors: unknown[]; @@ -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 + ); + } + 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 { diff --git a/src/dex-v2-worker.ts b/src/dex-v2-worker.ts new file mode 100644 index 0000000..693bb4b --- /dev/null +++ b/src/dex-v2-worker.ts @@ -0,0 +1,107 @@ +import { Lucid } from "@minswap/lucid-cardano"; + +import { BlockfrostAdapter, DexV2, DexV2Constant, OrderV2 } from "."; +import { NetworkEnvironment, NetworkId } from "./types/network"; +import { runRecurringJob } from "./utils/job"; + +type DexV2WorkerConstructor = { + networkEnv: NetworkEnvironment; + networkId: NetworkId; + lucid: Lucid; + blockfrostAdapter: BlockfrostAdapter; + privateKey: string; +}; + +export class DexV2Worker { + private readonly networkEnv: NetworkEnvironment; + private readonly networkId: NetworkId; + private readonly lucid: Lucid; + private readonly blockfrostAdapter: BlockfrostAdapter; + private readonly privateKey: string; + + constructor({ + networkEnv, + networkId, + lucid, + blockfrostAdapter, + privateKey, + }: DexV2WorkerConstructor) { + this.networkEnv = networkEnv; + this.networkId = networkId; + this.lucid = lucid; + this.blockfrostAdapter = blockfrostAdapter; + this.privateKey = privateKey; + } + + async start(): Promise { + await runRecurringJob({ + name: "lbe v2 batcher", + interval: 1000 * 30, // 30s + job: () => this.runWorker(), + }); + } + + async runWorker(): Promise { + const { orders: allOrders } = await this.blockfrostAdapter.getAllV2Orders(); + const currentSlot = await this.blockfrostAdapter.currentSlot(); + const currentTime = this.lucid.utils.slotToUnixTime(currentSlot); + const expiredOrders: OrderV2.State[] = []; + const mapDatum: Record = {}; + 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; + if (receiverDatum.type === OrderV2.ExtraDatumType.INLINE_DATUM) { + let rawDatum: string | undefined = undefined; + try { + rawDatum = await this.blockfrostAdapter.getDatumByDatumHash( + receiverDatum.hash + ); + // eslint-disable-next-line unused-imports/no-unused-vars + } catch (_err) { + continue; + } + mapDatum[receiverDatum.hash] = rawDatum; + } + expiredOrders.push(order); + if (expiredOrders.length === 20) { + break; + } + } + if (expiredOrders.length > 0) { + const orderUtxos = await this.lucid.utxosByOutRef( + expiredOrders.map((state) => ({ + txHash: state.txIn.txHash, + outputIndex: state.txIn.index, + })) + ); + 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}`); + } + } +} diff --git a/src/dex-v2.ts b/src/dex-v2.ts index 28161d7..c15288c 100644 --- a/src/dex-v2.ts +++ b/src/dex-v2.ts @@ -8,6 +8,7 @@ import { OutRef, Tx, TxComplete, + UnixTime, UTxO, } from "@minswap/lucid-cardano"; import invariant from "@minswap/tiny-invariant"; @@ -15,6 +16,7 @@ import invariant from "@minswap/tiny-invariant"; import { Asset, BlockfrostAdapter, + compareUtxo, DexV2Calculation, DexV2Constant, FIXED_DEPOSIT_ADA, @@ -190,6 +192,12 @@ export type CancelBulkOrdersOptions = { AuthorizationMethodType?: OrderV2.AuthorizationMethodType; }; +export type CancelExpiredOrderOptions = { + orderUtxos: UTxO[]; + currentSlot: UnixTime; + extraDatumMap: Record; +}; + export class DexV2 { private readonly lucid: Lucid; private readonly networkId: NetworkId; @@ -694,17 +702,6 @@ export class DexV2 { ); } - private getOrderScriptHash(): string | undefined { - const orderAddress = - DexV2Constant.CONFIG[this.networkId].orderEnterpriseAddress; - const addrDetails = this.lucid.utils.getAddressDetails(orderAddress); - invariant( - addrDetails.paymentCredential?.type === "Script", - "order address should be a script address" - ); - return addrDetails.paymentCredential.hash; - } - private getOrderMetadata(orderOption: OrderOptions): string { switch (orderOption.type) { case OrderV2.StepType.SWAP_EXACT_IN: { @@ -940,7 +937,6 @@ export class DexV2 { orderOutRefs, composeTx, }: CancelBulkOrdersOptions): Promise { - const v2OrderScriptHash = this.getOrderScriptHash(); const orderUtxos = await this.lucid.utxosByOutRef(orderOutRefs); if (orderUtxos.length === 0) { throw new Error("Order Utxos are empty"); @@ -962,7 +958,8 @@ export class DexV2 { this.lucid.utils.getAddressDetails(orderAddr).paymentCredential; invariant( orderScriptPaymentCred?.type === "Script" && - orderScriptPaymentCred.hash === v2OrderScriptHash, + orderScriptPaymentCred.hash === + DexV2Constant.CONFIG[this.networkId].orderScriptHash, `Utxo is not belonged Minswap's order address, utxo: ${utxo.txHash}` ); let datum: OrderV2.Datum; @@ -1003,4 +1000,104 @@ export class DexV2 { } return lucidTx.complete(); } + + async cancelExpiredOrders({ + orderUtxos, + currentSlot, + extraDatumMap, + }: CancelExpiredOrderOptions): Promise { + const refScript = await this.lucid.utxosByOutRef([ + DexV2Constant.DEPLOYED_SCRIPTS[this.networkId].order, + DexV2Constant.DEPLOYED_SCRIPTS[this.networkId].expiredOrderCancellation, + ]); + const currentTime = this.lucid.utils.slotToUnixTime(currentSlot); + invariant( + refScript.length === 2, + "cannot find deployed script for V2 Order or Expired Order Cancellation" + ); + const sortedOrderUtxos = [...orderUtxos].sort(compareUtxo); + + const lucidTx = this.lucid.newTx().readFrom(refScript); + lucidTx.collectFrom( + sortedOrderUtxos, + Data.to(new Constr(OrderV2.Redeemer.CANCEL_EXPIRED_ORDER_BY_ANYONE, [])) + ); + for (const orderUtxo of sortedOrderUtxos) { + const orderAddr = orderUtxo.address; + const orderScriptPaymentCred = + this.lucid.utils.getAddressDetails(orderAddr).paymentCredential; + invariant( + orderScriptPaymentCred?.type === "Script" && + orderScriptPaymentCred.hash === + DexV2Constant.CONFIG[this.networkId].orderScriptHash, + `Utxo is not belonged Minswap's order address, utxo: ${orderUtxo.txHash}` + ); + let datum: OrderV2.Datum; + if (orderUtxo.datum) { + const rawDatum = orderUtxo.datum; + datum = OrderV2.Datum.fromPlutusData( + this.networkId, + Data.from(rawDatum) + ); + } else if (orderUtxo.datumHash) { + const rawDatum = await this.lucid.datumOf(orderUtxo); + datum = OrderV2.Datum.fromPlutusData( + this.networkId, + rawDatum as Constr + ); + } else { + throw new Error( + "Utxo without Datum Hash or Inline Datum can not be spent" + ); + } + const expiryOptions = datum.expiredOptions; + invariant(expiryOptions !== undefined, "Order must have expiry options"); + invariant( + expiryOptions.maxCancellationTip >= DexV2Constant.DEFAULT_CANCEL_TIPS, + "Cancel tip is too low" + ); + invariant( + expiryOptions.expiredTime < BigInt(currentTime), + "Order is not expired" + ); + const refundDatum = datum.refundReceiverDatum; + const outAssets = { ...orderUtxo.assets }; + outAssets["lovelace"] -= expiryOptions.maxCancellationTip; + switch (refundDatum.type) { + case OrderV2.ExtraDatumType.NO_DATUM: { + lucidTx.payToAddress(datum.refundReceiver, outAssets); + break; + } + case OrderV2.ExtraDatumType.DATUM_HASH: { + lucidTx.payToAddressWithData( + datum.refundReceiver, + { hash: refundDatum.hash }, + outAssets + ); + break; + } + case OrderV2.ExtraDatumType.INLINE_DATUM: { + invariant( + refundDatum.hash in extraDatumMap, + `Can not find refund datum of order ${orderUtxo.txHash}#${orderUtxo.outputIndex}` + ); + lucidTx.payToAddressWithData( + datum.refundReceiver, + { inline: extraDatumMap[refundDatum.hash] }, + outAssets + ); + break; + } + } + } + lucidTx + .withdraw( + DexV2Constant.CONFIG[this.networkId].expiredOrderCancelAddress, + 0n, + Data.to(0n) + ) + .validFrom(currentTime) + .validTo(currentTime + 3 * 60 * 60 * 1000); + return await lucidTx.complete(); + } } diff --git a/src/types/constants.ts b/src/types/constants.ts index f72fb96..8989154 100644 --- a/src/types/constants.ts +++ b/src/types/constants.ts @@ -389,6 +389,7 @@ export namespace StableswapConstant { } export namespace DexV2Constant { + export const DEFAULT_CANCEL_TIPS = 300_000n; export type Config = { factoryAsset: string; poolAuthenAsset: string; @@ -397,6 +398,7 @@ export namespace DexV2Constant { globalSettingScriptHash: string; globalSettingScriptHashBech32: string; orderScriptHash: string; + orderScriptHashBech32: string; poolScriptHash: string; poolScriptHashBech32: string; poolCreationAddress: Address; @@ -432,6 +434,8 @@ export namespace DexV2Constant { "script1664wypvm4msc3a6fzayneamr0enee5sehham7nwtavwsk2s2vg9", orderScriptHash: "da9525463841173ad1230b1d5a1b5d0a3116bbdeb4412327148a1b7a", + orderScriptHashBech32: + "script1m22j233cgytn45frpvw45x6apgc3dw77k3qjxfc53gdh5cejhly", poolScriptHash: "d6ba9b7509eac866288ff5072d2a18205ac56f744bc82dcd808cb8fe", poolScriptHashBech32: @@ -465,6 +469,8 @@ export namespace DexV2Constant { "script17kqgctyepkrd549le97cnnhxa73qekzxzctrt9rcm945c880puk", orderScriptHash: "c3e28c36c3447315ba5a56f33da6a6ddc1770a876a8d9f0cb3a97c4c", + orderScriptHashBech32: + "script1c03gcdkrg3e3twj62menmf4xmhqhwz58d2xe7r9n497yc6r9qhd", poolScriptHash: "ea07b733d932129c378af627436e7cbc2ef0bf96e0036bb51b3bde6b", poolScriptHashBech32: @@ -665,29 +671,40 @@ export namespace LbeV2Constant { [NetworkId.MAINNET]: { factoryAsset: MAINNET_FACTORY_HASH + FACTORY_AUTH_AN, factoryHash: MAINNET_FACTORY_HASH, - factoryHashBech32: "script1m6550tz4ldxzcw9mzy6p72uzkttzuxsjqvc0stwpu4h26pl45ch", - factoryAddress: "addr1w802j3av2ha5ctpchvgng8ets2edvts6zgpnp7pdc8jkatgwxaxhw", - factoryRewardAddress: "stake17802j3av2ha5ctpchvgng8ets2edvts6zgpnp7pdc8jkatgjvaqtu", + factoryHashBech32: + "script1m6550tz4ldxzcw9mzy6p72uzkttzuxsjqvc0stwpu4h26pl45ch", + factoryAddress: + "addr1w802j3av2ha5ctpchvgng8ets2edvts6zgpnp7pdc8jkatgwxaxhw", + factoryRewardAddress: + "stake17802j3av2ha5ctpchvgng8ets2edvts6zgpnp7pdc8jkatgjvaqtu", treasuryAsset: MAINNET_FACTORY_HASH + TREASURY_AUTH_AN, treasuryHash: "1ce6abbd967cab867ad73855f8b154fcc57e41b15605b91590451650", - treasuryHashBech32: "script1rnn2h0vk0j4cv7kh8p2l3v25lnzhusd32czmj9vsg5t9q69xnhh", - treasuryAddress: "addr1wywwd2aaje72hpn66uu9t7932n7v2ljpk9tqtwg4jpz3v5qpqs70n", + treasuryHashBech32: + "script1rnn2h0vk0j4cv7kh8p2l3v25lnzhusd32czmj9vsg5t9q69xnhh", + treasuryAddress: + "addr1wywwd2aaje72hpn66uu9t7932n7v2ljpk9tqtwg4jpz3v5qpqs70n", managerAsset: MAINNET_FACTORY_HASH + MANAGER_AUTH_AN, managerHash: "e951d381ef510ae02b7496c2ff039e640ab2e2a561423d0cbf34b032", - managerHashBech32: "script1a9ga8q002y9wq2m5jmp07qu7vs9t9c49v9pr6r9lxjcry2xehgl", - managerAddress: "addr1w854r5upaags4cptwjtv9lcrnejq4vhz54s5y0gvhu6tqvsccjry6", + managerHashBech32: + "script1a9ga8q002y9wq2m5jmp07qu7vs9t9c49v9pr6r9lxjcry2xehgl", + managerAddress: + "addr1w854r5upaags4cptwjtv9lcrnejq4vhz54s5y0gvhu6tqvsccjry6", sellerAsset: MAINNET_FACTORY_HASH + SELLER_AUTH_AN, sellerHash: "ecf97d6f0ace26e69fa428610c7dbf5a686e1197f76511449d9a1b64", - sellerHashBech32: "script1anuh6mc2ecnwd8ay9psscldltf5xuyvh7aj3z3yangdkgh7ds8d", - sellerAddress: "addr1w8k0jlt0pt8zde5l5s5xzrrahadxsms3jlmk2y2ynkdpkeqn95g7r", + sellerHashBech32: + "script1anuh6mc2ecnwd8ay9psscldltf5xuyvh7aj3z3yangdkgh7ds8d", + sellerAddress: + "addr1w8k0jlt0pt8zde5l5s5xzrrahadxsms3jlmk2y2ynkdpkeqn95g7r", orderAsset: MAINNET_FACTORY_HASH + ORDER_AUTH_AN, orderHash: "5176775eed690d088bd29d9a6934b1e35ef1d897deb61d7b5dde11ca", - orderHashBech32: "script129m8whhddyxs3z7jnkdxjd93ud00rkyhm6mp676amcgu5kg5c44", - orderAddress: "addr1z9ghva67a45s6zyt62we56f5k834auwcjl0tv8tmth0prjjj2c79gy9l76sdg0xwhd7r0c0kna0tycz4y5s6mlenh8pqsk3urw", + orderHashBech32: + "script129m8whhddyxs3z7jnkdxjd93ud00rkyhm6mp676amcgu5kg5c44", + orderAddress: + "addr1z9ghva67a45s6zyt62we56f5k834auwcjl0tv8tmth0prjjj2c79gy9l76sdg0xwhd7r0c0kna0tycz4y5s6mlenh8pqsk3urw", }, }; diff --git a/src/types/order.ts b/src/types/order.ts index ee618e6..eea9334 100644 --- a/src/types/order.ts +++ b/src/types/order.ts @@ -3,6 +3,7 @@ import { Address, Constr, Data } from "@minswap/lucid-cardano"; import { AddressPlutusData } from "./address.internal"; import { Asset } from "./asset"; import { NetworkId } from "./network"; +import { TxIn, Value } from "./tx.internal"; export namespace OrderV1 { export enum StepType { @@ -1228,4 +1229,26 @@ export namespace OrderV2 { CANCEL_ORDER_BY_OWNER, CANCEL_EXPIRED_ORDER_BY_ANYONE, } + + export class State { + public readonly address: string; + public readonly txIn: TxIn; + public readonly value: Value; + public readonly datumCbor: string; + public readonly datum: Datum; + + constructor( + networkId: NetworkId, + address: string, + txIn: TxIn, + value: Value, + datum: string + ) { + this.address = address; + this.txIn = txIn; + this.value = value; + this.datumCbor = datum; + this.datum = Datum.fromPlutusData(networkId, Data.from(datum)); + } + } } From 276f91d2e27b63022be643af876180dbe1c8711a Mon Sep 17 00:00:00 2001 From: Ha Quang Minh Date: Tue, 26 Nov 2024 14:57:16 +0700 Subject: [PATCH 2/9] fix dex v2 worker --- examples/dex-v2-worker-example.ts | 3 -- src/dex-v2-worker.ts | 73 ++++++++++++++----------------- src/types/order.ts | 56 +++++++++++++++++++++--- 3 files changed, 85 insertions(+), 47 deletions(-) diff --git a/examples/dex-v2-worker-example.ts b/examples/dex-v2-worker-example.ts index c06ff5e..f94d69e 100644 --- a/examples/dex-v2-worker-example.ts +++ b/examples/dex-v2-worker-example.ts @@ -3,7 +3,6 @@ import { Network } from "@minswap/lucid-cardano"; import { BlockfrostAdapter, NetworkId } from "../src"; import { DexV2Worker } from "../src/dex-v2-worker"; -import { NetworkEnvironment } from "../src/types/network"; import { getBackendLucidInstance } from "../src/utils/lucid"; async function main(): Promise { @@ -29,8 +28,6 @@ async function main(): Promise { ); const worker = new DexV2Worker({ - networkEnv: NetworkEnvironment.TESTNET_PREPROD, - networkId: NetworkId.TESTNET, lucid, blockfrostAdapter, privateKey: diff --git a/src/dex-v2-worker.ts b/src/dex-v2-worker.ts index 693bb4b..4a24b40 100644 --- a/src/dex-v2-worker.ts +++ b/src/dex-v2-worker.ts @@ -1,33 +1,24 @@ -import { Lucid } from "@minswap/lucid-cardano"; +import { Data, Lucid } from "@minswap/lucid-cardano"; import { BlockfrostAdapter, DexV2, DexV2Constant, OrderV2 } from "."; -import { NetworkEnvironment, NetworkId } from "./types/network"; import { runRecurringJob } from "./utils/job"; type DexV2WorkerConstructor = { - networkEnv: NetworkEnvironment; - networkId: NetworkId; lucid: Lucid; blockfrostAdapter: BlockfrostAdapter; privateKey: string; }; export class DexV2Worker { - private readonly networkEnv: NetworkEnvironment; - private readonly networkId: NetworkId; private readonly lucid: Lucid; private readonly blockfrostAdapter: BlockfrostAdapter; private readonly privateKey: string; constructor({ - networkEnv, - networkId, lucid, blockfrostAdapter, privateKey, }: DexV2WorkerConstructor) { - this.networkEnv = networkEnv; - this.networkId = networkId; this.lucid = lucid; this.blockfrostAdapter = blockfrostAdapter; this.privateKey = privateKey; @@ -42,11 +33,10 @@ export class DexV2Worker { } async runWorker(): Promise { + 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 expiredOrders: OrderV2.State[] = []; - const mapDatum: Record = {}; for (const order of allOrders) { const orderDatum = order.datum; const expiredOptions = orderDatum.expiredOptions; @@ -62,46 +52,51 @@ export class DexV2Worker { continue; } + const mapDatum: Record = {}; const receiverDatum = orderDatum.refundReceiverDatum; + let rawDatum: string | undefined = undefined; if (receiverDatum.type === OrderV2.ExtraDatumType.INLINE_DATUM) { - let rawDatum: string | undefined = undefined; try { rawDatum = await this.blockfrostAdapter.getDatumByDatumHash( receiverDatum.hash ); + mapDatum[receiverDatum.hash] = rawDatum; // eslint-disable-next-line unused-imports/no-unused-vars } catch (_err) { continue; } - mapDatum[receiverDatum.hash] = rawDatum; } - expiredOrders.push(order); - if (expiredOrders.length === 20) { - break; - } - } - if (expiredOrders.length > 0) { - const orderUtxos = await this.lucid.utxosByOutRef( - expiredOrders.map((state) => ({ - txHash: state.txIn.txHash, - outputIndex: state.txIn.index, - })) - ); - const txComplete = await new DexV2( - this.lucid, - this.blockfrostAdapter - ).cancelExpiredOrders({ - orderUtxos: orderUtxos, - currentSlot, - extraDatumMap: mapDatum, - }); - const signedTx = await txComplete - .signWithPrivateKey(this.privateKey) - .complete(); + const orderUtxos = await this.lucid.utxosByOutRef([ + { + txHash: order.txIn.txHash, + outputIndex: order.txIn.index, + }, + ]); + if (orderUtxos.length === 0) { + continue; + } + try { + orderUtxos[0].datum = Data.to(OrderV2.Datum.toPlutusData(orderDatum)); + 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}`); + const txId = await signedTx.submit(); + console.info(`Transaction submitted successfully: ${txId}`); + break; + } catch (err) { + console.error(err); + continue; + } } } } diff --git a/src/types/order.ts b/src/types/order.ts index eea9334..27c6ad0 100644 --- a/src/types/order.ts +++ b/src/types/order.ts @@ -1,4 +1,5 @@ import { Address, Constr, Data } from "@minswap/lucid-cardano"; +import invariant from "@minswap/tiny-invariant"; import { AddressPlutusData } from "./address.internal"; import { Asset } from "./asset"; @@ -479,6 +480,11 @@ export namespace OrderV2 { export namespace AuthorizationMethod { export function fromPlutusData(data: Constr): AuthorizationMethod { let type: AuthorizationMethodType; + if (data.fields.length !== 1) { + throw Error( + `Field length of AuthorizationMethod must be in 1, actual: ${data.fields.length}` + ); + } switch (data.index) { case AuthorizationMethodType.SIGNATURE: { type = AuthorizationMethodType.SIGNATURE; @@ -1091,17 +1097,29 @@ export namespace OrderV2 { export function fromPlutusData(data: Constr): ExtraDatum { switch (data.index) { case ExtraDatumType.NO_DATUM: { + invariant( + data.fields.length === 0, + `Field Length of ExtraDatum.NO_DATUM must be 0, actually ${data.fields.length}` + ); return { type: ExtraDatumType.NO_DATUM, }; } case ExtraDatumType.DATUM_HASH: { + invariant( + data.fields.length === 1, + `Field Length of ExtraDatum.DATUM_HASH must be 1, actually ${data.fields.length}` + ); return { type: ExtraDatumType.DATUM_HASH, hash: data.fields[0] as string, }; } case ExtraDatumType.INLINE_DATUM: { + invariant( + data.fields.length === 1, + `Field Length of ExtraDatum.INLINE_DATUM must be 1, actually ${data.fields.length}` + ); return { type: ExtraDatumType.INLINE_DATUM, hash: data.fields[0] as string, @@ -1152,20 +1170,46 @@ export namespace OrderV2 { `Index of Order Datum must be 0, actual: ${data.index}` ); } + if (data.fields.length !== 9) { + throw new Error( + `Fields Length of Order Datum must be 9, actual: ${data.index}` + ); + } const maybeExpiry = data.fields[8] as Constr; let expiry: bigint[] | undefined; switch (maybeExpiry.index) { case 0: { - expiry = maybeExpiry.fields as bigint[]; - if (expiry.length !== 2) { + if (maybeExpiry.fields.length !== 1) { throw new Error( - `Order Expiry list must have 2 elements, actual: ${expiry.length}` + `Order maybeExpiry length must have 1 field, actual: ${maybeExpiry.fields.length}` ); } + const expiryOptions = maybeExpiry.fields[0] as Constr; + switch (expiryOptions.index) { + case 0: { + expiry = expiryOptions.fields as bigint[]; + if (expiry.length !== 2) { + throw new Error( + `Order Expiry list must have 2 elements, actual: ${expiry.length}` + ); + } + break; + } + default: { + throw new Error( + `Index of Expiry Options must have 1, actual: ${expiryOptions.index}` + ); + } + } break; } case 1: { expiry = undefined; + if (maybeExpiry.fields.length === 0) { + throw new Error( + `Order undefined Expiry must have 0 elements, actual: ${maybeExpiry.fields.length}` + ); + } break; } default: { @@ -1216,8 +1260,10 @@ export namespace OrderV2 { datum.maxBatcherFee, datum.expiredOptions ? new Constr(0, [ - datum.expiredOptions.expiredTime, - datum.expiredOptions.maxCancellationTip, + new Constr(0, [ + datum.expiredOptions.expiredTime, + datum.expiredOptions.maxCancellationTip, + ]), ]) : new Constr(1, []), ]); From 8ae4f559c8f1f792a9339964f7cede4200d698de Mon Sep 17 00:00:00 2001 From: Ha Quang Minh Date: Tue, 26 Nov 2024 16:13:02 +0700 Subject: [PATCH 3/9] fix --- src/adapter.ts | 26 ++++++++++++++------------ src/types/order.ts | 4 ++-- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/adapter.ts b/src/adapter.ts index e003aa1..bb6c3cc 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -594,18 +594,20 @@ export class BlockfrostAdapter implements Adapter { utxo.amount, utxo.inline_datum ); - } - 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 - ); + console.log(utxo.data_hash); + } 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}`); diff --git a/src/types/order.ts b/src/types/order.ts index 27c6ad0..7b85364 100644 --- a/src/types/order.ts +++ b/src/types/order.ts @@ -1197,7 +1197,7 @@ export namespace OrderV2 { } default: { throw new Error( - `Index of Expiry Options must have 1, actual: ${expiryOptions.index}` + `Index of Expiry Options must be 0, actual: ${expiryOptions.index}` ); } } @@ -1205,7 +1205,7 @@ export namespace OrderV2 { } case 1: { expiry = undefined; - if (maybeExpiry.fields.length === 0) { + if (maybeExpiry.fields.length !== 0) { throw new Error( `Order undefined Expiry must have 0 elements, actual: ${maybeExpiry.fields.length}` ); From ce9c8d2a69ae2d8da89552819869151b37e5ead3 Mon Sep 17 00:00:00 2001 From: Ha Quang Minh Date: Wed, 27 Nov 2024 10:32:19 +0700 Subject: [PATCH 4/9] fix order --- src/adapter.ts | 1 - src/dex-v2-worker.ts | 7 +++++-- src/dex-v2.ts | 5 ++++- src/types/constants.ts | 1 + src/types/order.ts | 28 ++++++++++------------------ 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/adapter.ts b/src/adapter.ts index bb6c3cc..314330c 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -594,7 +594,6 @@ export class BlockfrostAdapter implements Adapter { utxo.amount, utxo.inline_datum ); - console.log(utxo.data_hash); } else { if (utxo.data_hash !== null) { const orderDatum = await this.blockFrostApi.scriptsDatumCbor( diff --git a/src/dex-v2-worker.ts b/src/dex-v2-worker.ts index 4a24b40..f54cee8 100644 --- a/src/dex-v2-worker.ts +++ b/src/dex-v2-worker.ts @@ -93,8 +93,11 @@ export class DexV2Worker { const txId = await signedTx.submit(); console.info(`Transaction submitted successfully: ${txId}`); break; - } catch (err) { - console.error(err); + } catch (_err) { + console.log( + `Error order: order ${order.txIn.txHash}#${order.txIn.index}`, + _err + ); continue; } } diff --git a/src/dex-v2.ts b/src/dex-v2.ts index c15288c..49dfb4e 100644 --- a/src/dex-v2.ts +++ b/src/dex-v2.ts @@ -1097,7 +1097,10 @@ export class DexV2 { Data.to(0n) ) .validFrom(currentTime) - .validTo(currentTime + 3 * 60 * 60 * 1000); + .validTo(currentTime + 3 * 60 * 60 * 1000) + .attachMetadata(674, { + msg: [MetadataMessage.CANCEL_ORDER_AUTOMATICALLY], + }); return await lucidTx.complete(); } } diff --git a/src/types/constants.ts b/src/types/constants.ts index 8989154..84dfde3 100644 --- a/src/types/constants.ts +++ b/src/types/constants.ts @@ -769,6 +769,7 @@ export namespace LbeV2Constant { export enum MetadataMessage { DEPOSIT_ORDER = "SDK Minswap: Deposit Order", CANCEL_ORDER = "SDK Minswap: Cancel Order", + CANCEL_ORDER_AUTOMATICALLY = "SDK Minswap: Cancel Order Automatically", ZAP_IN_ORDER = "SDK Minswap: Zap Order", ZAP_OUT_ORDER = "SDK Minswap: Zap Out Order", SWAP_EXACT_IN_ORDER = "SDK Minswap: Swap Exact In Order", diff --git a/src/types/order.ts b/src/types/order.ts index 7b85364..9d6b98a 100644 --- a/src/types/order.ts +++ b/src/types/order.ts @@ -1184,23 +1184,15 @@ export namespace OrderV2 { `Order maybeExpiry length must have 1 field, actual: ${maybeExpiry.fields.length}` ); } - const expiryOptions = maybeExpiry.fields[0] as Constr; - switch (expiryOptions.index) { - case 0: { - expiry = expiryOptions.fields as bigint[]; - if (expiry.length !== 2) { - throw new Error( - `Order Expiry list must have 2 elements, actual: ${expiry.length}` - ); - } - break; - } - default: { - throw new Error( - `Index of Expiry Options must be 0, actual: ${expiryOptions.index}` - ); - } + if ( + !Array.isArray(maybeExpiry.fields[0]) || + maybeExpiry.fields[0].length !== 2 + ) { + throw new Error( + `maybeExpiry field0's length must be 2-element array, actual: ${maybeExpiry.fields[0]}` + ); } + expiry = maybeExpiry.fields[0] as bigint[]; break; } case 1: { @@ -1260,10 +1252,10 @@ export namespace OrderV2 { datum.maxBatcherFee, datum.expiredOptions ? new Constr(0, [ - new Constr(0, [ + [ datum.expiredOptions.expiredTime, datum.expiredOptions.maxCancellationTip, - ]), + ], ]) : new Constr(1, []), ]); From 2c77d0fa790572b2adefb3d9dd040b5f54db89de Mon Sep 17 00:00:00 2001 From: Ha Quang Minh Date: Wed, 27 Nov 2024 11:04:28 +0700 Subject: [PATCH 5/9] cancel multiple orders --- src/dex-v2-worker.ts | 76 +++++++++++++++++++++++------------------- src/dex-v2.ts | 2 +- src/types/constants.ts | 2 +- 3 files changed, 44 insertions(+), 36 deletions(-) diff --git a/src/dex-v2-worker.ts b/src/dex-v2-worker.ts index f54cee8..46d14ab 100644 --- a/src/dex-v2-worker.ts +++ b/src/dex-v2-worker.ts @@ -1,4 +1,4 @@ -import { Data, Lucid } from "@minswap/lucid-cardano"; +import { Lucid } from "@minswap/lucid-cardano"; import { BlockfrostAdapter, DexV2, DexV2Constant, OrderV2 } from "."; import { runRecurringJob } from "./utils/job"; @@ -37,6 +37,8 @@ export class DexV2Worker { const { orders: allOrders } = await this.blockfrostAdapter.getAllV2Orders(); const currentSlot = await this.blockfrostAdapter.currentSlot(); const currentTime = this.lucid.utils.slotToUnixTime(currentSlot); + const mapDatum: Record = {}; + const orders: OrderV2.State[] = []; for (const order of allOrders) { const orderDatum = order.datum; const expiredOptions = orderDatum.expiredOptions; @@ -51,8 +53,6 @@ export class DexV2Worker { ) { continue; } - - const mapDatum: Record = {}; const receiverDatum = orderDatum.refundReceiverDatum; let rawDatum: string | undefined = undefined; if (receiverDatum.type === OrderV2.ExtraDatumType.INLINE_DATUM) { @@ -66,40 +66,48 @@ export class DexV2Worker { continue; } } + orders.push(order); - const orderUtxos = await this.lucid.utxosByOutRef([ - { - txHash: order.txIn.txHash, - outputIndex: order.txIn.index, - }, - ]); - if (orderUtxos.length === 0) { - continue; - } - try { - orderUtxos[0].datum = Data.to(OrderV2.Datum.toPlutusData(orderDatum)); - 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}`); + // CANCEL MAX 20 Orders + if (orders.length === 20) { break; - } catch (_err) { - console.log( - `Error order: order ${order.txIn.txHash}#${order.txIn.index}`, - _err - ); - continue; } } + + 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.log( + `Error when the worker runs: orders ${orders.map((order) => `${order.txIn.txHash}#${order.txIn.index}`).join(", ")}`, + _err + ); + } } } diff --git a/src/dex-v2.ts b/src/dex-v2.ts index 49dfb4e..307f1d9 100644 --- a/src/dex-v2.ts +++ b/src/dex-v2.ts @@ -1099,7 +1099,7 @@ export class DexV2 { .validFrom(currentTime) .validTo(currentTime + 3 * 60 * 60 * 1000) .attachMetadata(674, { - msg: [MetadataMessage.CANCEL_ORDER_AUTOMATICALLY], + msg: [MetadataMessage.CANCEL_ORDERS_AUTOMATICALLY], }); return await lucidTx.complete(); } diff --git a/src/types/constants.ts b/src/types/constants.ts index 84dfde3..7c73380 100644 --- a/src/types/constants.ts +++ b/src/types/constants.ts @@ -769,7 +769,7 @@ export namespace LbeV2Constant { export enum MetadataMessage { DEPOSIT_ORDER = "SDK Minswap: Deposit Order", CANCEL_ORDER = "SDK Minswap: Cancel Order", - CANCEL_ORDER_AUTOMATICALLY = "SDK Minswap: Cancel Order Automatically", + CANCEL_ORDERS_AUTOMATICALLY = "SDK Minswap: Cancel Orders Automatically", ZAP_IN_ORDER = "SDK Minswap: Zap Order", ZAP_OUT_ORDER = "SDK Minswap: Zap Out Order", SWAP_EXACT_IN_ORDER = "SDK Minswap: Swap Exact In Order", From a754ebed49b0e2b70eb70a8f2834ed0cdf92d138 Mon Sep 17 00:00:00 2001 From: Ha Quang Minh Date: Wed, 27 Nov 2024 11:09:42 +0700 Subject: [PATCH 6/9] remove key --- examples/dex-v2-worker-example.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/dex-v2-worker-example.ts b/examples/dex-v2-worker-example.ts index f94d69e..f9ba0f6 100644 --- a/examples/dex-v2-worker-example.ts +++ b/examples/dex-v2-worker-example.ts @@ -7,7 +7,7 @@ import { getBackendLucidInstance } from "../src/utils/lucid"; async function main(): Promise { const network: Network = "Preprod"; - const blockfrostProjectId = "preprodSj4PM4LDOTa2BbfAY4XIEqASI9gKzOEz"; + const blockfrostProjectId = ""; const blockfrostUrl = "https://cardano-preprod.blockfrost.io/api/v0"; const address = @@ -30,8 +30,7 @@ async function main(): Promise { const worker = new DexV2Worker({ lucid, blockfrostAdapter, - privateKey: - "ed25519e_sk1pqs6ssazw755demuks2974mdwu6stz0uxpj543edm5cm0y96p9gk9lcv5jspdg3aq7wtv9r96uaru0rnu4qdm7lccarntjm22mtk72cm5cjrj", + privateKey: "", }); await worker.start(); From ec6b7ee5b4af684f9587474b0e38c1d81967119e Mon Sep 17 00:00:00 2001 From: Ha Quang Minh Date: Wed, 27 Nov 2024 11:25:10 +0700 Subject: [PATCH 7/9] docs --- docs/dex-v2-worker.md | 44 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 docs/dex-v2-worker.md diff --git a/docs/dex-v2-worker.md b/docs/dex-v2-worker.md new file mode 100644 index 0000000..466dda1 --- /dev/null +++ b/docs/dex-v2-worker.md @@ -0,0 +1,44 @@ +# LBE V2 Documentation + +## Overview + +This documentation provides guidelines for tracking and canceling expired AMM V2 orders. Every 30 seconds, the service checks and cancels expired orders. + +### Transaction Builder Function + +- **DexV2Worker Class**: Located in `src/dex-v2-worker.ts`. +- **DexV2Worker Example**: Located in `examples/dex-v2-worker-example.ts`. + +## Example Usage + +### Run Worker Example + +```ts +const network: Network = "Preprod"; +const blockfrostProjectId = ""; +const blockfrostUrl = "https://cardano-preprod.blockfrost.io/api/v0"; + +const 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: "", +}); + +await worker.start(); +``` From 408860ef3c941a86bfd6de3aed1ec786fb889f08 Mon Sep 17 00:00:00 2001 From: Ha Quang Minh Date: Wed, 27 Nov 2024 14:30:18 +0700 Subject: [PATCH 8/9] fix comment --- docs/{transaction.md => dex-transaction.md} | 91 +++++++++++++++------ docs/dex-v2-worker.md | 44 ---------- src/adapter.ts | 25 +++--- src/dex-v2-worker.ts | 7 +- src/dex-v2.ts | 6 +- 5 files changed, 90 insertions(+), 83 deletions(-) rename docs/{transaction.md => dex-transaction.md} (80%) delete mode 100644 docs/dex-v2-worker.md diff --git a/docs/transaction.md b/docs/dex-transaction.md similarity index 80% rename from docs/transaction.md rename to docs/dex-transaction.md index df95b92..a9dc2fe 100644 --- a/docs/transaction.md +++ b/docs/dex-transaction.md @@ -1,4 +1,4 @@ -# Minswap AMM V2 & Stableswap Classes Documentation +# Minswap DEX Classes documentation ## Overview @@ -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 @@ -54,7 +56,10 @@ const blockfrostAdapter = new BlockfrostAdapter( const utxos = await lucid.utxosAt(address); const lpAsset = Asset.fromString(""); -const config = StableswapConstant.getConfigByLpAsset(lpAsset, NetworkId.TESTNET); +const config = StableswapConstant.getConfigByLpAsset( + lpAsset, + NetworkId.TESTNET +); const pool = await blockfrostAdapter.getStablePoolByLpAsset(lpAsset); @@ -92,7 +97,9 @@ const txComplete = await new Stableswap(lucid).createBulkOrdersTx({ ], }); -const signedTx = await txComplete.signWithPrivateKey("").complete(); +const signedTx = await txComplete + .signWithPrivateKey("") + .complete(); const txId = await signedTx.submit(); console.info(`Transaction submitted successfully: ${txId}`); ``` @@ -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("").complete(); +const signedTx = await txComplete + .signWithPrivateKey("") + .complete(); const txId = await signedTx.submit(); console.info(`Transaction submitted successfully: ${txId}`); - ``` ### 3. Create the DEX V2 Liquiditiy Pool @@ -204,19 +214,54 @@ const txComplete = await new DexV2(lucid, blockfrostAdapter).createPoolTx({ tradingFeeNumerator: 100n, }); -const signedTx = await txComplete.signWithPrivateKey("").complete(); +const signedTx = await txComplete + .signWithPrivateKey("") + .complete(); const txId = await signedTx.submit(); console.info(`Transaction submitted successfully: ${txId}`); ``` +### 4. Run Dex V2 Worker + +```ts +const network: Network = "Preprod"; +const blockfrostProjectId = ""; +const blockfrostUrl = "https://cardano-preprod.blockfrost.io/api/v0"; + +const 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: "", +}); + +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) \ No newline at end of file +- [AMM V2 class](../src/dex-v2.ts) diff --git a/docs/dex-v2-worker.md b/docs/dex-v2-worker.md deleted file mode 100644 index 466dda1..0000000 --- a/docs/dex-v2-worker.md +++ /dev/null @@ -1,44 +0,0 @@ -# LBE V2 Documentation - -## Overview - -This documentation provides guidelines for tracking and canceling expired AMM V2 orders. Every 30 seconds, the service checks and cancels expired orders. - -### Transaction Builder Function - -- **DexV2Worker Class**: Located in `src/dex-v2-worker.ts`. -- **DexV2Worker Example**: Located in `examples/dex-v2-worker-example.ts`. - -## Example Usage - -### Run Worker Example - -```ts -const network: Network = "Preprod"; -const blockfrostProjectId = ""; -const blockfrostUrl = "https://cardano-preprod.blockfrost.io/api/v0"; - -const 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: "", -}); - -await worker.start(); -``` diff --git a/src/adapter.ts b/src/adapter.ts index 314330c..9fabb77 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -594,20 +594,19 @@ export class BlockfrostAdapter implements Adapter { 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 - ); - } + } 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}`); } diff --git a/src/dex-v2-worker.ts b/src/dex-v2-worker.ts index 46d14ab..610922f 100644 --- a/src/dex-v2-worker.ts +++ b/src/dex-v2-worker.ts @@ -55,7 +55,10 @@ export class DexV2Worker { } const receiverDatum = orderDatum.refundReceiverDatum; let rawDatum: string | undefined = undefined; - if (receiverDatum.type === OrderV2.ExtraDatumType.INLINE_DATUM) { + if ( + receiverDatum.type === OrderV2.ExtraDatumType.INLINE_DATUM || + receiverDatum.type === OrderV2.ExtraDatumType.DATUM_HASH + ) { try { rawDatum = await this.blockfrostAdapter.getDatumByDatumHash( receiverDatum.hash @@ -104,7 +107,7 @@ export class DexV2Worker { const txId = await signedTx.submit(); console.info(`Transaction submitted successfully: ${txId}`); } catch (_err) { - console.log( + console.error( `Error when the worker runs: orders ${orders.map((order) => `${order.txIn.txHash}#${order.txIn.index}`).join(", ")}`, _err ); diff --git a/src/dex-v2.ts b/src/dex-v2.ts index 307f1d9..24c3735 100644 --- a/src/dex-v2.ts +++ b/src/dex-v2.ts @@ -1069,9 +1069,13 @@ export class DexV2 { break; } case OrderV2.ExtraDatumType.DATUM_HASH: { + invariant( + refundDatum.hash in extraDatumMap, + `Can not find refund datum of order ${orderUtxo.txHash}#${orderUtxo.outputIndex}` + ); lucidTx.payToAddressWithData( datum.refundReceiver, - { hash: refundDatum.hash }, + { asHash: extraDatumMap[refundDatum.hash] }, outAssets ); break; From a78e34f3a98914af9ddf201c6c405937892e58a4 Mon Sep 17 00:00:00 2001 From: Ha Quang Minh Date: Wed, 27 Nov 2024 15:43:31 +0700 Subject: [PATCH 9/9] rename function+title+file and handle refund datum hash without raw datum --- docs/dex-transaction.md | 10 +++++----- ...ker-example.ts => expired-order-monitor-example.ts} | 6 +++--- src/dex-v2.ts | 8 +++----- src/{dex-v2-worker.ts => expired-order-monitor.ts} | 7 +++++-- 4 files changed, 16 insertions(+), 15 deletions(-) rename examples/{dex-v2-worker-example.ts => expired-order-monitor-example.ts} (87%) rename src/{dex-v2-worker.ts => expired-order-monitor.ts} (93%) diff --git a/docs/dex-transaction.md b/docs/dex-transaction.md index a9dc2fe..249b90e 100644 --- a/docs/dex-transaction.md +++ b/docs/dex-transaction.md @@ -9,8 +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`. +- **ExpiredOrderMonitor Class**: Located in `src/expired-order-monitor.ts`. +- **ExpiredOrderMonitor Example**: Located in `examples/expired-order-monitor-example.ts`. ### Utility Functions @@ -221,7 +221,7 @@ const txId = await signedTx.submit(); console.info(`Transaction submitted successfully: ${txId}`); ``` -### 4. Run Dex V2 Worker +### 4. Off-chain component to track and cancel the expired orders ```ts const network: Network = "Preprod"; @@ -244,13 +244,13 @@ const blockfrostAdapter = new BlockfrostAdapter( }) ); -const worker = new DexV2Worker({ +const monitor = new ExpiredOrderMonitor({ lucid, blockfrostAdapter, privateKey: "", }); -await worker.start(); +await monitor.start(); ``` ## Additional Examples diff --git a/examples/dex-v2-worker-example.ts b/examples/expired-order-monitor-example.ts similarity index 87% rename from examples/dex-v2-worker-example.ts rename to examples/expired-order-monitor-example.ts index f9ba0f6..3b85155 100644 --- a/examples/dex-v2-worker-example.ts +++ b/examples/expired-order-monitor-example.ts @@ -2,7 +2,7 @@ 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 { ExpiredOrderMonitor } from "../src/expired-order-monitor"; import { getBackendLucidInstance } from "../src/utils/lucid"; async function main(): Promise { @@ -27,13 +27,13 @@ async function main(): Promise { }) ); - const worker = new DexV2Worker({ + const monitor = new ExpiredOrderMonitor({ lucid, blockfrostAdapter, privateKey: "", }); - await worker.start(); + await monitor.start(); } void main(); diff --git a/src/dex-v2.ts b/src/dex-v2.ts index 24c3735..a94f05c 100644 --- a/src/dex-v2.ts +++ b/src/dex-v2.ts @@ -1069,13 +1069,11 @@ export class DexV2 { break; } case OrderV2.ExtraDatumType.DATUM_HASH: { - invariant( - refundDatum.hash in extraDatumMap, - `Can not find refund datum of order ${orderUtxo.txHash}#${orderUtxo.outputIndex}` - ); lucidTx.payToAddressWithData( datum.refundReceiver, - { asHash: extraDatumMap[refundDatum.hash] }, + refundDatum.hash in extraDatumMap + ? { asHash: extraDatumMap[refundDatum.hash] } + : { hash: refundDatum.hash }, outAssets ); break; diff --git a/src/dex-v2-worker.ts b/src/expired-order-monitor.ts similarity index 93% rename from src/dex-v2-worker.ts rename to src/expired-order-monitor.ts index 610922f..98ec277 100644 --- a/src/dex-v2-worker.ts +++ b/src/expired-order-monitor.ts @@ -9,7 +9,7 @@ type DexV2WorkerConstructor = { privateKey: string; }; -export class DexV2Worker { +export class ExpiredOrderMonitor { private readonly lucid: Lucid; private readonly blockfrostAdapter: BlockfrostAdapter; private readonly privateKey: string; @@ -66,7 +66,10 @@ export class DexV2Worker { mapDatum[receiverDatum.hash] = rawDatum; // eslint-disable-next-line unused-imports/no-unused-vars } catch (_err) { - continue; + if (receiverDatum.type === OrderV2.ExtraDatumType.INLINE_DATUM) { + // if receiver Datum type is INLINE_DATUM, skip this order. + continue; + } } } orders.push(order);