diff --git a/bun.lockb b/bun.lockb index 8aecd97..b00f4cc 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 7cba8b4..3938b97 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ }, "dependencies": { "@minswap/tiny-invariant": "^1.2.0", - "@minswap/translucent": "0.0.6-minswap.17", + "@minswap/translucent": "0.0.6-minswap.18", "rollup": "^4.14.1", "rollup-plugin-dts": "^6.1.0", "rollup-plugin-esbuild": "^6.1.1", diff --git a/src/batcher.ts b/src/batcher.ts new file mode 100644 index 0000000..28c6d60 --- /dev/null +++ b/src/batcher.ts @@ -0,0 +1,559 @@ +import { WarehouseBuilder, type WarehouseBuilderOptions } from "./build-tx"; +import { genWarehouseOptions, hexToUtxo } from "./tests/utils"; +import type { + AmmPoolDatum, + BluePrintAsset, + MaestroSupportedNetworks, + ManagerDatum, + Translucent, + TreasuryDatum, + UTxO, + UnixTime, +} from "./types"; +import * as T from "@minswap/translucent"; +import { calculateInitialLiquidity, computeLPAssetName } from "./utils"; +import { + MINIMUM_ORDER_COLLECTED, + MINIMUM_ORDER_REDEEMED, + MINIMUM_SELLER_COLLECTED, +} from "./constants"; +import invariant from "@minswap/tiny-invariant"; +import lbeV2Script from "./../lbe-v2-script.json"; + +function getParams() { + const network: MaestroSupportedNetworks = process + .argv[2] as MaestroSupportedNetworks; + const maestroApiKey = process.argv[3]; + const seedPhase = process.argv[4]; + return { + network, + maestroApiKey, + seedPhase, + }; +} + +let warehouse: { + treasuryUTxO: UTxO; + treasuryDatum: TreasuryDatum; + managerUTxO?: UTxO; + managerDatum?: ManagerDatum; + sellerUTxOs: UTxO[]; + uncollectedOrderUTxOs: UTxO[]; + collectedOrderUTxOs: UTxO[]; + warehouseOptions: WarehouseBuilderOptions; + t: Translucent; + lbeId: string; +}; +function getMapLbeIdUtxO< + T extends { baseAsset: BluePrintAsset; raiseAsset: BluePrintAsset }, +>(utxos: UTxO[], toDatum: (arg0: string) => T): Record { + const mapLbeIdUTxO: Record = {}; + for (const utxo of utxos) { + const { datum } = utxo; + const { baseAsset, raiseAsset } = toDatum(datum!); + const lbeId = computeLPAssetName( + baseAsset.policyId + baseAsset.assetName, + raiseAsset.policyId + raiseAsset.assetName, + ); + if (mapLbeIdUTxO[lbeId]) { + mapLbeIdUTxO[lbeId] = [utxo]; + } else { + mapLbeIdUTxO[lbeId].push(utxo); + } + } + return mapLbeIdUTxO; +} + +async function getLBEUTxO({ + t, + txHash, + builder, +}: { + t: Translucent; + txHash: string; + builder: WarehouseBuilder; +}) { + let index = 0; + const outputs: { + treasuryUTxO?: UTxO; + managerUTxO?: UTxO; + orderUTxOs: UTxO[]; + } = { + orderUTxOs: [], + }; + while (true) { + const utxo = (await t.utxosByOutRef([{ txHash, outputIndex: index }]))[0]; + if (utxo === undefined) { + return outputs; + } + + // handle + const { address, assets } = utxo; + const hash = T.paymentCredentialOf(address).hash; + if (hash === builder.orderHash && assets[builder.orderToken] === 1n) { + outputs.orderUTxOs.push(utxo); + } + if (hash === builder.managerHash && assets[builder.managerToken] === 1n) { + outputs.managerUTxO = utxo; + } + if (hash === builder.treasuryHash && assets[builder.treasuryToken] === 1n) { + outputs.treasuryUTxO = utxo; + } + ++index; + } +} + +async function collectSellers({ sellerUTxOs }: { sellerUTxOs: UTxO[] }) { + const { treasuryUTxO, managerUTxO, warehouseOptions, t } = warehouse; + const builder = new WarehouseBuilder(warehouseOptions); + builder.buildCollectSeller({ + treasuryRefInput: treasuryUTxO, + managerInput: managerUTxO!, + sellerInputs: sellerUTxOs, + validFrom: Date.now() / 1000, + validTo: Date.now() / 1000 + 60 * 3, + }); + const tx = await builder.complete().complete(); + const signedTx = await tx.sign().complete(); + const txHash = await signedTx.submit(); + await t.awaitTx(txHash, 1000); + const output = await getLBEUTxO({ t, txHash, builder }); + warehouse.managerUTxO = output.managerUTxO!; + warehouse.managerDatum = WarehouseBuilder.fromDatumManager( + output.managerUTxO!.datum!, + ); +} + +async function collectManager() { + const { treasuryUTxO, managerUTxO, warehouseOptions, t } = warehouse; + const builder = new WarehouseBuilder(warehouseOptions); + builder.buildCollectManager({ + treasuryInput: treasuryUTxO, + managerInput: managerUTxO!, + validFrom: Date.now() / 1000, + validTo: Date.now() / 1000 + 60 * 3, + }); + const tx = await builder.complete().complete(); + const signedTx = await tx.sign().complete(); + const txHash = await signedTx.submit(); + await t.awaitTx(txHash, 1000); + const output = await getLBEUTxO({ t, txHash, builder }); + warehouse = { + ...warehouse, + treasuryUTxO: output.treasuryUTxO!, + treasuryDatum: WarehouseBuilder.fromDatumTreasury( + output.treasuryUTxO!.datum!, + ), + }; +} + +async function collectOrders({ + uncollectedOrderUTxOs, +}: { + uncollectedOrderUTxOs: UTxO[]; +}) { + const { warehouseOptions, treasuryUTxO, t } = warehouse; + const builder = new WarehouseBuilder(warehouseOptions); + builder.buildCollectOrders({ + treasuryInput: treasuryUTxO, + orderInputs: uncollectedOrderUTxOs, + validFrom: Date.now() / 1000, + validTo: Date.now() / 1000 + 60 * 3, + }); + const tx = await builder.complete().complete(); + const signedTx = await tx.sign().complete(); + const txHash = await signedTx.submit(); + await t.awaitTx(txHash, 1000); + const output = await getLBEUTxO({ t, txHash, builder }); + warehouse.treasuryUTxO = output.treasuryUTxO!; + warehouse.treasuryDatum = WarehouseBuilder.fromDatumTreasury( + output.treasuryUTxO!.datum!, + ); + warehouse.collectedOrderUTxOs = output.orderUTxOs; +} + +async function findFactoryUTxO( + t: Translucent, + builder: WarehouseBuilder, + lpAssetName: string, +): Promise { + const utxos = await t.utxosAtWithUnit( + { type: "Script", hash: builder.ammFactoryHash }, + builder.ammFactoryToken, + ); + for (const utxo of utxos) { + const { head, tail } = WarehouseBuilder.fromDatumAmmFactory(utxo.datum!); + if (lpAssetName > head && lpAssetName < tail) { + return utxo; + } + } + throw Error(`Can not find factory for lpAssetName: ${lpAssetName}`); +} + +function sortPairAsset( + baseAsset: BluePrintAsset, + raiseAsset: BluePrintAsset, +): [BluePrintAsset, BluePrintAsset] { + if (baseAsset.policyId !== raiseAsset.policyId) { + if (baseAsset.policyId !== raiseAsset.policyId) { + return [baseAsset, raiseAsset]; + } + return [raiseAsset, baseAsset]; + } + if (baseAsset.assetName < raiseAsset.assetName) { + return [baseAsset, raiseAsset]; + } + return [raiseAsset, baseAsset]; +} +async function createAmmPool() { + const { treasuryUTxO, treasuryDatum, warehouseOptions, t } = warehouse; + const builder = new WarehouseBuilder(warehouseOptions); + const { + baseAsset, + raiseAsset, + poolAllocation, + maximumRaise, + minimumRaise, + reserveBase, + reserveRaise, + totalPenalty, + poolBaseFee, + } = treasuryDatum; + let finalReserveRaise = reserveRaise + totalPenalty; + if (maximumRaise && reserveRaise + totalPenalty > maximumRaise) { + finalReserveRaise = maximumRaise; + } + if (minimumRaise) { + invariant( + finalReserveRaise > minimumRaise, + "not raise enough to create pool and this LBE will be cancelled next round", + ); + } + const [assetA, assetB] = sortPairAsset(baseAsset, raiseAsset); + const [lbeReserveA, lbeReserveB] = + baseAsset.policyId === assetA.policyId && + baseAsset.assetName === assetA.assetName + ? [reserveBase, finalReserveRaise] + : [reserveBase, finalReserveRaise]; + const poolReserveA = (lbeReserveA * poolAllocation) / 100n; + const poolReserveB = (lbeReserveB * poolAllocation) / 100n; + const lpAssetName = computeLPAssetName( + assetA.policyId + assetA.assetName, + assetB.policyId + assetB.assetName, + ); + + const totalLpToken = calculateInitialLiquidity(poolReserveA, poolReserveB); + const ammPoolDatum: AmmPoolDatum = { + poolBatchingStakeCredential: { + Inline: [ + { + ScriptCredential: [ + t.utils.validatorToScriptHash( + builder.ammValidators.poolBatchingValidator, + ), + ], + }, + ], + }, + assetA: assetA, + assetB: assetB, + totalLiquidity: totalLpToken, + reserveA: poolReserveA, + reserveB: poolReserveB, + baseFeeANumerator: poolBaseFee, + baseFeeBNumerator: poolBaseFee, + feeSharingNumeratorOpt: null, + allowDynamicFee: false, + }; + const factoryUTxO = await findFactoryUTxO(t, builder, lpAssetName); + builder.buildCreateAmmPool({ + treasuryInput: treasuryUTxO, + ammFactoryInput: factoryUTxO, + ammPoolDatum: ammPoolDatum, + totalLiquidity: totalLpToken, + receiverA: lbeReserveA - poolReserveA, + receiverB: lbeReserveB - poolReserveB, + validFrom: Date.now() / 1000, + validTo: Date.now() / 1000 + 60 * 3, + }); + const tx = await builder.complete().complete(); + const signedTx = await tx.sign().complete(); + const txHash = await signedTx.submit(); + await t.awaitTx(txHash, 1000); + const output = await getLBEUTxO({ t, txHash, builder }); + warehouse.treasuryUTxO = output.treasuryUTxO!; + warehouse.treasuryDatum = WarehouseBuilder.fromDatumTreasury( + output.treasuryUTxO!.datum!, + ); +} + +async function redeemOrders({ orderUTxOs }: { orderUTxOs: UTxO[] }) { + const { treasuryUTxO, warehouseOptions, t } = warehouse; + const builder = new WarehouseBuilder(warehouseOptions); + builder.buildRedeemOrders({ + treasuryInput: treasuryUTxO, + orderInputs: orderUTxOs, + validFrom: Date.now() / 1000, + validTo: Date.now() / 1000 + 60 * 3, + }); + const tx = await builder.complete().complete(); + const signedTx = await tx.sign().complete(); + const txHash = await signedTx.submit(); + await t.awaitTx(txHash, 1000); + const output = await getLBEUTxO({ t, txHash, builder }); + warehouse.treasuryUTxO = output.treasuryUTxO!; + warehouse.treasuryDatum = WarehouseBuilder.fromDatumTreasury( + output.treasuryUTxO!.datum!, + ); +} + +async function refundOrders({ orderUTxOs }: { orderUTxOs: UTxO[] }) { + const { treasuryUTxO, warehouseOptions, t } = warehouse; + const builder = new WarehouseBuilder(warehouseOptions); + builder.buildRefundOrders({ + treasuryInput: treasuryUTxO, + orderInputs: orderUTxOs, + validFrom: Date.now() / 1000, + validTo: Date.now() / 1000 + 60 * 3, + }); + const tx = await builder.complete().complete(); + const signedTx = await tx.sign().complete(); + const txHash = await signedTx.submit(); + await t.awaitTx(txHash, 1000); + const output = await getLBEUTxO({ t, txHash, builder }); + warehouse.treasuryUTxO = output.treasuryUTxO!; + warehouse.treasuryDatum = WarehouseBuilder.fromDatumTreasury( + output.treasuryUTxO!.datum!, + ); +} + +async function coutingPhase() { + if (warehouse.treasuryDatum.isManagerCollected === false) { + invariant(warehouse.managerUTxO); + while (warehouse.sellerUTxOs.length > 0) { + const collectAmount = Math.min( + Number(MINIMUM_SELLER_COLLECTED), + warehouse.sellerUTxOs.length, + ); + if ( + collectAmount < MINIMUM_SELLER_COLLECTED && + BigInt(collectAmount) !== warehouse.managerDatum?.sellerCount + ) { + throw Error( + `Can not find remaining sellers to collect in LBE ${warehouse.lbeId}`, + ); + } + await collectSellers({ + sellerUTxOs: warehouse.sellerUTxOs.splice(0, collectAmount), + }); + } + await collectManager(); + } + while (warehouse.uncollectedOrderUTxOs.length > 0) { + // TODO: invariant if batcher miss some uncollected orders + await collectOrders({ + uncollectedOrderUTxOs: warehouse.uncollectedOrderUTxOs.splice( + 0, + Math.min( + warehouse.uncollectedOrderUTxOs.length, + Number(MINIMUM_ORDER_COLLECTED), + ), + ), + }); + } +} + +async function redeemLpTokenPhase() { + while (warehouse.collectedOrderUTxOs.length !== 0) { + // TODO: invariant if batcher miss some collected orders + await redeemOrders({ + orderUTxOs: warehouse.collectedOrderUTxOs.splice( + 0, + Math.min( + warehouse.collectedOrderUTxOs.length, + Number(MINIMUM_ORDER_REDEEMED), + ), + ), + }); + } +} + +async function refundPhase() { + while (warehouse.collectedOrderUTxOs.length !== 0) { + // TODO: invariant if batcher miss some collected orders + await refundOrders({ + orderUTxOs: warehouse.collectedOrderUTxOs.splice( + 0, + Math.min( + warehouse.collectedOrderUTxOs.length, + Number(MINIMUM_ORDER_REDEEMED), + ), + ), + }); + } +} + +async function batchLBE(): Promise { + await coutingPhase(); + if (warehouse.treasuryDatum.isCancelled) { + await refundPhase(); + } else { + if (warehouse.treasuryDatum.totalLiquidity === 0n) { + await createAmmPool(); + } + await redeemLpTokenPhase(); + } +} + +async function cancelLBE() { + // TODO +} + +async function handleLBE(options: { + t: Translucent; + treasuryUTxO: UTxO; + managerUTxO?: UTxO; + sellerUTxOs: UTxO[]; + orderUTxOs: UTxO[]; + current: UnixTime; +}) { + const { t, treasuryUTxO, managerUTxO, sellerUTxOs, orderUTxOs, current } = + options; + const warehouseOptions = await genWarehouseOptions(t); + const builder = new WarehouseBuilder(warehouseOptions); + const { datum } = treasuryUTxO; + const treasuryDatum = WarehouseBuilder.fromDatumTreasury(datum!); + const { baseAsset, raiseAsset } = treasuryDatum; + + const lbeId = computeLPAssetName( + baseAsset.policyId + baseAsset.assetName, + raiseAsset.policyId + raiseAsset.assetName, + ); + warehouse = { + treasuryUTxO, + treasuryDatum: treasuryDatum, + managerUTxO, + managerDatum: managerUTxO + ? WarehouseBuilder.fromDatumManager(managerUTxO!.datum!) + : undefined, + sellerUTxOs, + uncollectedOrderUTxOs: [], + collectedOrderUTxOs: [], + t, + warehouseOptions, + lbeId, + }; + for (const utxo of orderUTxOs) { + const orderDatum = WarehouseBuilder.fromDatumOrder(utxo.datum!); + if (orderDatum.isCollected) { + warehouse.collectedOrderUTxOs.push(utxo); + } else { + warehouse.uncollectedOrderUTxOs.push(utxo); + } + } + + await cancelLBE(); + if ( + warehouse.treasuryDatum.endTime < current || + warehouse.treasuryDatum.isCancelled === true + ) { + await batchLBE(); + } +} + +async function runBatcher(t: Translucent): Promise { + const warehouseOptions = await genWarehouseOptions(t); + warehouseOptions.deployedValidators = { + treasuryValidator: hexToUtxo(lbeV2Script.treasuryRefInput), + managerValidator: hexToUtxo(lbeV2Script.managerRefInput), + sellerValidator: hexToUtxo(lbeV2Script.sellerRefInput), + orderValidator: hexToUtxo(lbeV2Script.orderRefInput), + factoryValidator: hexToUtxo(lbeV2Script.factoryRefInput), + }; + warehouseOptions.ammDeployedValidators = { + authenValidator: hexToUtxo(lbeV2Script.ammAuthenRefInput), + poolValidator: hexToUtxo(lbeV2Script.ammPoolRefInput), + factoryValidator: hexToUtxo(lbeV2Script.factoryRefInput), + }; + const builder = new WarehouseBuilder(warehouseOptions); + const treasuryUTxOs = await t.utxosAtWithUnit( + { + type: "Script", + hash: builder.treasuryHash, + }, + builder.treasuryToken, + ); + const managerUTxOs = await t.utxosAtWithUnit( + { + type: "Script", + hash: builder.managerHash, + }, + builder.managerToken, + ); + const sellerUTxOs = await t.utxosAtWithUnit( + { + type: "Script", + hash: builder.sellerHash, + }, + builder.sellerToken, + ); + const orderUTxOs = await t.utxosAtWithUnit( + { + type: "Script", + hash: builder.orderHash, + }, + builder.orderToken, + ); + + const mapOrder: Record = getMapLbeIdUtxO( + orderUTxOs, + WarehouseBuilder.fromDatumOrder, + ); + const mapSeller: Record = getMapLbeIdUtxO( + sellerUTxOs, + WarehouseBuilder.fromDatumSeller, + ); + const mapManager: Record = getMapLbeIdUtxO( + managerUTxOs, + WarehouseBuilder.fromDatumManager, + ); + const currentDate = Date.now() / 1000; + for (const utxo of treasuryUTxOs) { + const { datum } = utxo; + const treasuryDatum = WarehouseBuilder.fromDatumTreasury(datum!); + const { baseAsset, raiseAsset, collectedFund, totalLiquidity } = + treasuryDatum; + if (totalLiquidity !== 0n && collectedFund === 0n) { + // finished LBE + continue; + } + const lbeId = computeLPAssetName( + baseAsset.policyId + baseAsset.assetName, + raiseAsset.policyId + raiseAsset.assetName, + ); + try { + await handleLBE({ + t, + treasuryUTxO: utxo, + managerUTxO: mapManager[lbeId][0] ?? undefined, + sellerUTxOs: mapSeller[lbeId] ?? [], + orderUTxOs: mapOrder[lbeId] ?? [], + current: currentDate, + }); + } catch (err) { + console.error(err); + } + } +} + +async function main(): Promise { + const { network, seedPhase, maestroApiKey } = getParams(); + const maestro = new T.Maestro({ network, apiKey: maestroApiKey }); + const t = await T.Translucent.new(maestro, network); + t.selectWalletFromSeed(seedPhase); + while (true) { + await runBatcher(t); + } +} + +void main(); diff --git a/src/build-tx.ts b/src/build-tx.ts index dc4824d..9bc0789 100644 --- a/src/build-tx.ts +++ b/src/build-tx.ts @@ -45,6 +45,7 @@ import type { } from "./deploy-validators"; import type { Address, + AmmFactoryDatum, AmmPoolDatum, Assets, BluePrintAsset, @@ -191,11 +192,10 @@ export type BuildCloseEventOptions = { }; export class WarehouseBuilder { + // immutable t: Translucent; validators: Validators; deployedValidators: DeployedValidators; - tx: Tx; - tasks: Array<() => void> = []; // Validator Hash factoryHash: string; @@ -221,6 +221,22 @@ export class WarehouseBuilder { orderToken: string; sellerToken: string; + // AMM + ammValidators: MinswapValidators; + ammDeployedValidators: DeployMinswapValidators; + ammFactoryAddress: Address; + ammPoolAddress: Address; + ammAuthenHash: string; + ammPoolHash: string; + ammFactoryHash: string; + ammPoolToken: string; + ammFactoryToken: string; + // ----------------------------------- + + // mutable + tx: Tx; + tasks: Array<() => void> = []; + // Reference Input treasuryRefInput: UTxO | undefined; @@ -239,22 +255,12 @@ export class WarehouseBuilder { orderRedeemer: OrderRedeemer | undefined; mintRedeemer: MintRedeemer | undefined; - // AMM - ammValidators: MinswapValidators; - ammDeployedValidators: DeployMinswapValidators; - ammFactoryAddress: Address; - ammPoolAddress: Address; - ammAuthenHash: string; - ammPoolHash: string; - ammFactoryHash: string; - ammPoolToken: string; - ammFactoryToken: string; - // Internal Asset baseAsset: BluePrintAsset | undefined; raiseAsset: BluePrintAsset | undefined; lpAssetName: string | undefined; ammLpToken: string | undefined; + // ----------------------------------- constructor(options: WarehouseBuilderOptions) { const { @@ -339,6 +345,31 @@ export class WarehouseBuilder { return this.tx; } + public clean(): WarehouseBuilder { + this.tx = this.t.newTx(); + this.tasks = []; + this.treasuryRefInput = undefined; + this.treasuryInputs = []; + this.managerInputs = []; + this.factoryInputs = []; + this.orderInputs = []; + this.sellerInputs = []; + + this.factoryRedeemer = undefined; + this.treasuryRedeemer = undefined; + this.managerRedeemer = undefined; + this.sellerRedeemer = undefined; + this.orderRedeemer = undefined; + this.mintRedeemer = undefined; + + this.baseAsset = undefined; + this.raiseAsset = undefined; + this.lpAssetName = undefined; + this.ammLpToken = undefined; + + return this; + } + public buildInitFactory(options: BuildInitFactoryOptions): WarehouseBuilder { const { seedUtxo } = options; this.tasks.push( @@ -448,7 +479,7 @@ export class WarehouseBuilder { owner, } = options; invariant(managerUtxo.datum); - const managerInDatum = this.fromDatumManager(managerUtxo.datum); + const managerInDatum = WarehouseBuilder.fromDatumManager(managerUtxo.datum); const managerOutDatum = { ...managerInDatum, sellerCount: managerInDatum.sellerCount + addSellerCount, @@ -494,7 +525,7 @@ export class WarehouseBuilder { orderOutputDatums, } = options; invariant(sellerUtxo.datum); - const sellerInDatum = this.fromDatumSeller(sellerUtxo.datum); + const sellerInDatum = WarehouseBuilder.fromDatumSeller(sellerUtxo.datum); this.setInnerAssets(sellerInDatum.baseAsset, sellerInDatum.raiseAsset); let inputAmount = 0n; let inputPenaltyAmount = 0n; @@ -502,7 +533,7 @@ export class WarehouseBuilder { let outputPenaltyAmount = 0n; for (const o of orderInputs) { invariant(o.datum); - const datum = this.fromDatumOrder(o.datum); + const datum = WarehouseBuilder.fromDatumOrder(o.datum); inputAmount += datum.amount; inputPenaltyAmount += datum.penaltyAmount; } @@ -578,7 +609,7 @@ export class WarehouseBuilder { penaltyConfig, } = options; invariant(treasuryInput.datum); - const inDatum = this.fromDatumTreasury(treasuryInput.datum); + const inDatum = WarehouseBuilder.fromDatumTreasury(treasuryInput.datum); const treasuryOutDatum: TreasuryDatum = { ...inDatum, startTime: startTime ?? inDatum.startTime, @@ -616,7 +647,9 @@ export class WarehouseBuilder { const { treasuryInput, validFrom, validTo, ammFactoryRefInput, reason } = options; invariant(treasuryInput.datum); - const treasuryInDatum = this.fromDatumTreasury(treasuryInput.datum); + const treasuryInDatum = WarehouseBuilder.fromDatumTreasury( + treasuryInput.datum, + ); const treasuryOutDatum: TreasuryDatum = { ...treasuryInDatum, isCancelled: true, @@ -664,7 +697,9 @@ export class WarehouseBuilder { extraDatum, } = options; invariant(treasuryInput.datum); - const treasuryInDatum = this.fromDatumTreasury(treasuryInput.datum); + const treasuryInDatum = WarehouseBuilder.fromDatumTreasury( + treasuryInput.datum, + ); const totalLbeLPs = totalLiquidity - LP_COLATERAL; const receiverLP = (totalLbeLPs * (treasuryInDatum.poolAllocation - 50n)) / @@ -742,7 +777,9 @@ export class WarehouseBuilder { public buildCloseEvent(options: BuildCloseEventOptions): WarehouseBuilder { const { treasuryInput, factoryInputs, validFrom, validTo } = options; invariant(treasuryInput.datum); - const treasuryDatum = this.fromDatumTreasury(treasuryInput.datum); + const treasuryDatum = WarehouseBuilder.fromDatumTreasury( + treasuryInput.datum, + ); const innerFactoryRedeemer = { baseAsset: treasuryDatum.baseAsset, raiseAsset: treasuryDatum.raiseAsset, @@ -790,7 +827,9 @@ export class WarehouseBuilder { ): WarehouseBuilder { const { treasuryInput, orderInputs, validFrom, validTo } = options; invariant(treasuryInput.datum); - const treasuryInDatum = this.fromDatumTreasury(treasuryInput.datum); + const treasuryInDatum = WarehouseBuilder.fromDatumTreasury( + treasuryInput.datum, + ); this.setInnerAssets(treasuryInDatum.baseAsset, treasuryInDatum.raiseAsset); invariant(this.ammLpToken); const sortedOrders = sortUTxOs(orderInputs); @@ -812,7 +851,7 @@ export class WarehouseBuilder { ); for (const order of sortedOrders) { invariant(order.datum); - const datum = this.fromDatumOrder(order.datum); + const datum = WarehouseBuilder.fromDatumOrder(order.datum); const lpAmount = (datum.amount * treasuryInDatum.totalLiquidity) / treasuryInDatum.reserveRaise; @@ -876,12 +915,15 @@ export class WarehouseBuilder { ); return this; } + public buildRefundOrders( options: BuildRedeemOrdersOptions, ): WarehouseBuilder { const { treasuryInput, orderInputs, validFrom, validTo } = options; invariant(treasuryInput.datum); - const treasuryInDatum = this.fromDatumTreasury(treasuryInput.datum); + const treasuryInDatum = WarehouseBuilder.fromDatumTreasury( + treasuryInput.datum, + ); this.setInnerAssets(treasuryInDatum.baseAsset, treasuryInDatum.raiseAsset); invariant(this.ammLpToken); const sortedOrders = sortUTxOs(orderInputs); @@ -894,7 +936,9 @@ export class WarehouseBuilder { ); for (const order of sortedOrders) { invariant(order.datum); - const { penaltyAmount, amount, owner } = this.fromDatumOrder(order.datum); + const { penaltyAmount, amount, owner } = WarehouseBuilder.fromDatumOrder( + order.datum, + ); const assets: Record = { lovelace: ORDER_MIN_ADA, }; @@ -959,7 +1003,9 @@ export class WarehouseBuilder { ): WarehouseBuilder { const { treasuryInput, orderInputs, validFrom, validTo } = options; invariant(treasuryInput.datum); - const treasuryInDatum = this.fromDatumTreasury(treasuryInput.datum); + const treasuryInDatum = WarehouseBuilder.fromDatumTreasury( + treasuryInput.datum, + ); let treasuryOutDatum: TreasuryDatum = { ...treasuryInDatum, }; @@ -970,7 +1016,7 @@ export class WarehouseBuilder { for (const o of sortedOrders) { invariant(o.datum); const datum: OrderDatum = { - ...this.fromDatumOrder(o.datum), + ...WarehouseBuilder.fromDatumOrder(o.datum), isCollected: true, }; orderOutDatums.push(datum); @@ -1015,9 +1061,13 @@ export class WarehouseBuilder { ): WarehouseBuilder { const { treasuryInput, managerInput, validFrom, validTo } = options; invariant(treasuryInput.datum); - const treasuryInDatum = this.fromDatumTreasury(treasuryInput.datum); + const treasuryInDatum = WarehouseBuilder.fromDatumTreasury( + treasuryInput.datum, + ); invariant(managerInput.datum); - const managerInDatum = this.fromDatumManager(managerInput.datum); + const managerInDatum = WarehouseBuilder.fromDatumManager( + managerInput.datum, + ); const treasuryOutDatum = options.treasuryOutDatum ?? { ...treasuryInDatum, reserveRaise: managerInDatum.reserveRaise, @@ -1057,7 +1107,7 @@ export class WarehouseBuilder { const { treasuryRefInput, managerInput, sellerInputs, validFrom, validTo } = options; invariant(managerInput.datum); - const managerInDatum: ManagerDatum = this.fromDatumManager( + const managerInDatum = WarehouseBuilder.fromDatumManager( managerInput.datum, ); invariant( @@ -1069,7 +1119,7 @@ export class WarehouseBuilder { let totalPenalty = 0n; for (const seller of sellerInputs) { invariant(seller.datum); - const datum = this.fromDatumSeller(seller.datum); + const datum = WarehouseBuilder.fromDatumSeller(seller.datum); totalReserveRaise += datum.amount; totalPenalty += datum.penaltyAmount; } @@ -1103,7 +1153,7 @@ export class WarehouseBuilder { }, () => { for (const utxo of sellerInputs) { - const sellerDatum = this.fromDatumSeller(utxo.datum!); + const sellerDatum = WarehouseBuilder.fromDatumSeller(utxo.datum!); const assets = { lovelace: utxo.assets["lovelace"] - COLLECT_SELLER_COMMISSION, }; @@ -1121,77 +1171,81 @@ export class WarehouseBuilder { } /************************* PARSER *************************/ - fromDatumTreasury(rawDatum: string): TreasuryDatum { + static fromDatumTreasury(rawDatum: string): TreasuryDatum { return T.Data.from( rawDatum, TreasuryValidateTreasurySpending.treasuryInDatum, ); } - toDatumTreasury(datum: TreasuryDatum): string { + static toDatumTreasury(datum: TreasuryDatum): string { return T.Data.to(datum, TreasuryValidateTreasurySpending.treasuryInDatum); } - fromDatumFactory(rawDatum: string): FactoryDatum { + static fromDatumFactory(rawDatum: string): FactoryDatum { return T.Data.from(rawDatum, FactoryValidateFactory.datum); } - toDatumFactory(datum: FactoryDatum) { + static toDatumFactory(datum: FactoryDatum) { return T.Data.to(datum, FactoryValidateFactory.datum); } - fromDatumSeller(rawDatum: string): SellerDatum { + static fromDatumSeller(rawDatum: string): SellerDatum { return T.Data.from(rawDatum, SellerValidateSellerSpending.sellerInDatum); } - toDatumSeller(datum: SellerDatum): string { + static toDatumSeller(datum: SellerDatum): string { return T.Data.to(datum, SellerValidateSellerSpending.sellerInDatum); } - fromDatumOrder(rawDatum: string): OrderDatum { + static fromDatumOrder(rawDatum: string): OrderDatum { return T.Data.from(rawDatum, OrderValidateOrder.datum); } - toDatumOrder(datum: OrderDatum): string { + static toDatumOrder(datum: OrderDatum): string { return T.Data.to(datum, OrderValidateOrder.datum); } - fromDatumManager(rawDatum: string): ManagerDatum { + static fromDatumManager(rawDatum: string): ManagerDatum { return T.Data.from(rawDatum, ManagerValidateManagerSpending.managerInDatum); } - toDatumManager(datum: ManagerDatum): string { + static toDatumManager(datum: ManagerDatum): string { return T.Data.to(datum, ManagerValidateManagerSpending.managerInDatum); } - toRedeemerOrder(redeemer: OrderRedeemer): string { + static toRedeemerOrder(redeemer: OrderRedeemer): string { return T.Data.to(redeemer, OrderValidateOrder.redeemer); } - toRedeemerTreasury(redeemer: TreasuryRedeemer): string { + static toRedeemerTreasury(redeemer: TreasuryRedeemer): string { return T.Data.to(redeemer, TreasuryValidateTreasurySpending.redeemer); } - toRedeemerManager(redeemer: ManagerRedeemer): string { + static toRedeemerManager(redeemer: ManagerRedeemer): string { return T.Data.to(redeemer, ManagerValidateManagerSpending.redeemer); } - toRedeemerSellerSpend(redeemer: SellerRedeemer): string { + static toRedeemerSellerSpend(redeemer: SellerRedeemer): string { return T.Data.to(redeemer, SellerValidateSellerSpending.redeemer); } - toRedeemerFactory(redeemer: FactoryRedeemer): string { + static toRedeemerFactory(redeemer: FactoryRedeemer): string { return T.Data.to(redeemer, FactoryValidateFactory.redeemer); } - toRedeemerMinting(redeemer: MintRedeemer): string { + static toRedeemerMinting(redeemer: MintRedeemer): string { return T.Data.to(redeemer, FactoryValidateFactoryMinting.redeemer); } - toDatumAmmPool(datum: AmmPoolDatum): string { + static toDatumAmmPool(datum: AmmPoolDatum): string { return T.Data.to(datum, FeedTypeAmmPool._datum); } + static fromDatumAmmFactory(rawDatum: string): AmmFactoryDatum { + return T.Data.from(rawDatum, AmmValidateFactory.datum); + } + calFinalReserveRaise(datum: TreasuryDatum) { if ( datum.maximumRaise && @@ -1243,7 +1297,7 @@ export class WarehouseBuilder { .readFrom([this.deployedValidators["managerValidator"]]) .collectFrom( this.managerInputs, - this.toRedeemerManager(this.managerRedeemer), + WarehouseBuilder.toRedeemerManager(this.managerRedeemer), ); } @@ -1258,7 +1312,7 @@ export class WarehouseBuilder { .readFrom([this.deployedValidators["sellerValidator"]]) .collectFrom( this.sellerInputs, - this.toRedeemerSellerSpend(this.sellerRedeemer), + WarehouseBuilder.toRedeemerSellerSpend(this.sellerRedeemer), ); } @@ -1271,7 +1325,7 @@ export class WarehouseBuilder { .readFrom([this.deployedValidators["factoryValidator"]]) .collectFrom( this.factoryInputs, - this.toRedeemerFactory(this.factoryRedeemer), + WarehouseBuilder.toRedeemerFactory(this.factoryRedeemer), ); } @@ -1282,7 +1336,10 @@ export class WarehouseBuilder { invariant(this.orderRedeemer); this.tx .readFrom([this.deployedValidators["orderValidator"]]) - .collectFrom(this.orderInputs, this.toRedeemerOrder(this.orderRedeemer)); + .collectFrom( + this.orderInputs, + WarehouseBuilder.toRedeemerOrder(this.orderRedeemer), + ); } spendingTreasuryInput() { @@ -1294,7 +1351,7 @@ export class WarehouseBuilder { .readFrom([this.deployedValidators["treasuryValidator"]]) .collectFrom( this.treasuryInputs, - this.toRedeemerTreasury(this.treasuryRedeemer), + WarehouseBuilder.toRedeemerTreasury(this.treasuryRedeemer), ); } @@ -1311,7 +1368,7 @@ export class WarehouseBuilder { this.tx.payToAddressWithData( this.treasuryAddress, { - inline: this.toDatumTreasury(treasuryOutDatum), + inline: WarehouseBuilder.toDatumTreasury(treasuryOutDatum), }, assets, ); @@ -1410,7 +1467,7 @@ export class WarehouseBuilder { this.tx.payToAddressWithData( this.managerAddress, { - inline: this.toDatumManager(datum), + inline: WarehouseBuilder.toDatumManager(datum), }, { [this.managerToken]: 1n, @@ -1440,7 +1497,7 @@ export class WarehouseBuilder { this.tx.payToAddressWithData( this.orderAddress, { - inline: this.toDatumOrder(datum), + inline: WarehouseBuilder.toDatumOrder(datum), }, assets, ); @@ -1455,7 +1512,7 @@ export class WarehouseBuilder { this.tx.payToAddressWithData( this.sellerAddress, { - inline: this.toDatumSeller(datum), + inline: WarehouseBuilder.toDatumSeller(datum), }, { [this.sellerToken]: 1n, @@ -1470,7 +1527,7 @@ export class WarehouseBuilder { this.tx.payToAddressWithData( this.sellerAddress, { - inline: this.toDatumSeller(datum), + inline: WarehouseBuilder.toDatumSeller(datum), }, assets, ); @@ -1517,7 +1574,7 @@ export class WarehouseBuilder { this.tx.payToAddressWithData( this.factoryAddress, { - inline: this.toDatumFactory(datum), + inline: WarehouseBuilder.toDatumFactory(datum), }, { [this.factoryToken]: 1n, @@ -1540,7 +1597,9 @@ export class WarehouseBuilder { invariant(this.factoryInputs.length == 1); invariant(this.factoryInputs[0].datum); invariant(this.lpAssetName); - const factoryDatum = this.fromDatumFactory(this.factoryInputs[0].datum); + const factoryDatum = WarehouseBuilder.fromDatumFactory( + this.factoryInputs[0].datum, + ); const newFactoryHeadDatum: FactoryDatum = { head: factoryDatum.head, tail: this.lpAssetName, @@ -1558,8 +1617,8 @@ export class WarehouseBuilder { const [headInput, tailInput] = this.factoryInputs; invariant(headInput.datum); invariant(tailInput.datum); - const headDatum = this.fromDatumFactory(headInput.datum); - const tailDatum = this.fromDatumFactory(tailInput.datum); + const headDatum = WarehouseBuilder.fromDatumFactory(headInput.datum); + const tailDatum = WarehouseBuilder.fromDatumFactory(tailInput.datum); const newDatum = { head: headDatum.head, tail: tailDatum.tail, @@ -1596,7 +1655,7 @@ export class WarehouseBuilder { .withdraw( this.factoryRewardAddress, 0n, - this.toRedeemerMinting("ManageOrder"), + WarehouseBuilder.toRedeemerMinting("ManageOrder"), ); } @@ -1612,7 +1671,7 @@ export class WarehouseBuilder { { [this.treasuryToken]: amount, }, - this.toRedeemerMinting(this.mintRedeemer), + WarehouseBuilder.toRedeemerMinting(this.mintRedeemer), ); } @@ -1623,7 +1682,7 @@ export class WarehouseBuilder { { [this.managerToken]: amount, }, - this.toRedeemerMinting(this.mintRedeemer), + WarehouseBuilder.toRedeemerMinting(this.mintRedeemer), ); } @@ -1636,7 +1695,7 @@ export class WarehouseBuilder { { [this.sellerToken]: mintAmount, }, - this.toRedeemerMinting(this.mintRedeemer), + WarehouseBuilder.toRedeemerMinting(this.mintRedeemer), ); } @@ -1649,7 +1708,7 @@ export class WarehouseBuilder { { [this.orderToken]: mintAmount, }, - this.toRedeemerMinting(this.mintRedeemer), + WarehouseBuilder.toRedeemerMinting(this.mintRedeemer), ); } @@ -1678,7 +1737,7 @@ export class WarehouseBuilder { { [this.factoryToken]: amount, }, - this.toRedeemerMinting(redeemer), + WarehouseBuilder.toRedeemerMinting(redeemer), ); } @@ -1765,7 +1824,7 @@ export class WarehouseBuilder { .payToAddressWithData( this.ammPoolAddress, { - inline: this.toDatumAmmPool(poolDatum), + inline: WarehouseBuilder.toDatumAmmPool(poolDatum), }, poolAssets, ); diff --git a/src/chaining.ts b/src/chaining.ts new file mode 100644 index 0000000..b993bcd --- /dev/null +++ b/src/chaining.ts @@ -0,0 +1,144 @@ +import * as T from "@minswap/translucent"; + +import type { + CTransactionHash, + CTransactionOutputs, + CTransactionUnspentOutputs, + MaestroSupportedNetworks, + TxSigned, + UTxO, +} from "./types"; + +export type InputId = string; +export type InputIdentify = ( + txHash: CTransactionHash, + finalOutputs: CTransactionOutputs, +) => CTransactionUnspentOutputs; + +export type Chaining = { + mapInputs: Record; + inputIdentifyFuncs: Record; + buildTx: ( + mapInputs: Record, + extra: any, + ) => Promise; + stopCondition: () => boolean; + submitTx: (tx: string) => Promise; +}; + +export async function doChaining(options: Chaining, mutate?: any) { + let { mapInputs, inputIdentifyFuncs, buildTx, stopCondition, submitTx } = + options; + let C = T.CModuleLoader.get; + while (true) { + if (stopCondition()) { + return; + } + let tx = await buildTx(mapInputs, mutate); + await submitTx(tx.toString()); + let transactionHash = tx.toHash(); + let txHash = C.TransactionHash.from_hex(transactionHash); + let finalOutputs = tx.txSigned.body().outputs(); + for (let [k, v] of Object.entries(inputIdentifyFuncs)) { + let coreUtxos = v(txHash, finalOutputs); + let utxos: UTxO[] = []; + for (let i = 0; i < coreUtxos.len(); i++) { + let coreUtxo = coreUtxos.get(i); + utxos.push(T.coreToUtxo(coreUtxo)); + } + mapInputs[k] = utxos; + } + } +} + +let submitTx = async (tx: string): Promise => { + console.log({ tx }); + const response = await fetch("https://dev-3.minswap.org/graphql", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: `{"query":"mutation SubmitTx($tx: String!) {submitTx(tx: $tx)}","variables":{"tx":"${tx}"}}`, + }); + const result = await response.text(); + return result; +}; + +let main = async () => { + await T.loadModule(); + await T.CModuleLoader.load(); + let network: MaestroSupportedNetworks = "Preprod"; + let maestroApiKey = "E0n5jUy4j40nhKCuB7LrYabTNieG0egu" ?? process.argv[2]; + let seedPhase = + "muffin spell cement resemble frame pupil grow gloom hawk wild item hungry polar ice maximum sport economy drop sun timber stone circle army jazz" ?? + process.argv[3]; + + const maestro = new T.Maestro({ network, apiKey: maestroApiKey }); + const t = await T.Translucent.new(maestro, network); + t.selectWalletFromSeed(seedPhase); + + let address = await t.wallet.address(); + let C = T.CModuleLoader.get; + + let inputIdentifyFuncs = { + root: (txHash: CTransactionHash, finalOutputs: CTransactionOutputs) => { + console.log("finalOutputs", finalOutputs.to_js_value()); + let result = C.TransactionUnspentOutputs.new(); + for (let i = 0; i <= finalOutputs.len(); i++) { + let output = finalOutputs.get(i); + console.log("output", output.to_js_value()); + let curAddress = output.address(); + if ( + curAddress.to_bech32() === address && + BigInt(output.amount().coin().to_str()) >= 500_000_000n + ) { + let input = C.TransactionInput.new( + txHash, + C.BigNum.from_str(i.toString()), + ); + result.add(C.TransactionUnspentOutput.new(input, output)); + break; + } + } + return result; + }, + }; + + let count = 0; + let stopCondition = () => { + console.log({ count }); + return count++ >= 4; + }; + + let mapInputs = { + root: (await t.utxosAt(address)).filter( + (u) => u.assets["lovelace"] >= 500_000_000n, + ), + }; + let buildTx = async ( + mapInputs: Record, + ): Promise => { + let txBuilder = t.newTx(); + txBuilder + .collectFrom(mapInputs["root"]) + .payToAddress(address, { lovelace: 3_000_000n }); + let tx = await txBuilder.complete(); + let signedTx = tx.sign(); + let txSigned = await signedTx.complete(); + console.log("txSigned", txSigned.txSigned.to_js_value()); + return txSigned; + }; + + let options: Chaining = { + mapInputs, + inputIdentifyFuncs, + buildTx, + stopCondition, + submitTx: async (tx: string) => { + submitTx(tx); + }, + }; + await doChaining(options); +}; + +// main(); diff --git a/src/cmd/batcher.ts b/src/cmd/batcher.ts new file mode 100644 index 0000000..73bb553 --- /dev/null +++ b/src/cmd/batcher.ts @@ -0,0 +1,360 @@ +import * as T from "@minswap/translucent"; +import { + WarehouseBuilder, + type BuildCollectManagerOptions, + type BuildCollectSellersOptions, +} from "../build-tx"; +import { + doChaining, + type Chaining, + type InputId, + type InputIdentify, +} from "../chaining"; +import type { + Address, + BluePrintAsset, + CTransactionHash, + CTransactionOutputs, + Tx, + TxSigned, + UTxO, + Unit, +} from "../types"; +import { catchWrapper, computeLPAssetName } from "../utils"; +import { MINIMUM_SELLER_COLLECTED } from ".."; + +type LbeId = string; +type LbePairAsset = { baseAsset: BluePrintAsset; raiseAsset: BluePrintAsset }; +type LbeValidator = "treasury" | "manager" | "seller" | "order"; +type LbePhase = + | "collectOrders" + | "collectSellers" + | "collectManager" + | "refundOrders" + | "redeemOrders" + | "createAmmPool"; +type LbeUTxO = { + treasury: UTxO; + manager?: UTxO; + sellers: UTxO[]; + orders: UTxO[]; +}; + +interface CollectSellersMapInput extends Record { + seed: UTxO[]; + manager: UTxO[]; +} + +namespace LbeId { + export function fromUtxo(options: { tag: LbeValidator; utxo: UTxO }): LbeId { + let { tag, utxo } = options; + + // return LbeId from LbePairAsset + let innerFunc = ({ baseAsset, raiseAsset }: LbePairAsset) => + computeLPAssetName( + baseAsset.policyId + baseAsset.assetName, + raiseAsset.policyId + raiseAsset.assetName, + ); + + let cases: Record LbePairAsset> = { + treasury: (u: UTxO) => WarehouseBuilder.fromDatumTreasury(u.datum!), + manager: (u: UTxO) => WarehouseBuilder.fromDatumManager(u.datum!), + seller: (u: UTxO) => WarehouseBuilder.fromDatumSeller(u.datum!), + order: (u: UTxO) => WarehouseBuilder.fromDatumOrder(u.datum!), + }; + + return innerFunc(cases[tag](utxo)); + } +} + +namespace LbePhase { + export function fromLbeUtxo(lbeUtxo: LbeUTxO): LbePhase | undefined { + let things: { checker: (lbeUtxo: LbeUTxO) => boolean; phase: LbePhase }[] = + [ + { checker: isCollectOrders, phase: "collectOrders" }, + { checker: isCollectSellers, phase: "collectSellers" }, + { checker: isCollectManager, phase: "collectManager" }, + { checker: isRefundOrders, phase: "refundOrders" }, + { checker: isRedeemOrders, phase: "redeemOrders" }, + { checker: isCreateAmmPool, phase: "createAmmPool" }, + ]; + for (let { checker, phase } of things) { + if (catchWrapper(checker, lbeUtxo, false)) { + return phase; + } + } + return undefined; + } + + function isCollectOrders(lbeUtxo: LbeUTxO) { + let treasuryDatum = WarehouseBuilder.fromDatumTreasury( + lbeUtxo.treasury.datum!, + ); + if (treasuryDatum.isCancelled) { + // collected Manager but not collect all funds yet + return ( + treasuryDatum.isManagerCollected && + treasuryDatum.collectedFund !== + treasuryDatum.reserveRaise + treasuryDatum.totalPenalty + ); + } else { + // collected Manager but not created pool yet. + return ( + treasuryDatum.isManagerCollected && treasuryDatum.totalLiquidity === 0n + ); + } + } + + function isCollectSellers(lbeUtxo: LbeUTxO) { + let treasuryDatum = WarehouseBuilder.fromDatumTreasury( + lbeUtxo.treasury.datum!, + ); + let managerDatum = WarehouseBuilder.fromDatumManager( + lbeUtxo.manager!.datum!, + ); + // LBE is cancelled or Discovery phase ended + // Still have Sellers left + return ( + (treasuryDatum.isCancelled || treasuryDatum.endTime < Date.now()) && + managerDatum.sellerCount > 0n + ); + } + + function isCollectManager(lbeUtxo: LbeUTxO) { + let managerDatum = WarehouseBuilder.fromDatumManager( + lbeUtxo.manager!.datum!, + ); + // Collected all Sellers + return managerDatum.sellerCount === 0n; + } + + function isRefundOrders(lbeUtxo: LbeUTxO) { + let treasuryDatum = WarehouseBuilder.fromDatumTreasury( + lbeUtxo.treasury.datum!, + ); + return ( + treasuryDatum.isCancelled && + treasuryDatum.collectedFund === + treasuryDatum.reserveRaise + treasuryDatum.totalPenalty + ); + } + + function isRedeemOrders(lbeUtxo: LbeUTxO) { + let treasuryDatum = WarehouseBuilder.fromDatumTreasury( + lbeUtxo.treasury.datum!, + ); + return treasuryDatum.totalLiquidity > 0n && lbeUtxo.orders.length > 0; + } + + function isCreateAmmPool(lbeUtxo: LbeUTxO) { + let treasuryDatum = WarehouseBuilder.fromDatumTreasury( + lbeUtxo.treasury.datum!, + ); + return treasuryDatum.totalLiquidity === 0n && !treasuryDatum.isCancelled; + } +} + +export class WarehouseBatcher { + builder: WarehouseBuilder; + mapLbeUTxO: Record = {}; + + constructor(builder: WarehouseBuilder) { + this.builder = builder; + } + + async submitTx(tx: string) { + await this.builder.t.provider.submitTx(tx); + } + + // IDENTIFY + identifyCommon(address: Address): InputIdentify { + let innerFunc = ( + txHash: CTransactionHash, + finalOutputs: CTransactionOutputs, + ) => { + let C = T.CModuleLoader.get; + let result = C.TransactionUnspentOutputs.new(); + for (let i = 0; i <= finalOutputs.len(); i++) { + let output = finalOutputs.get(i); + let curAddress = output.address(); + if (curAddress.to_bech32() === address) { + let input = C.TransactionInput.new( + txHash, + C.BigNum.from_str(i.toString()), + ); + result.add(C.TransactionUnspentOutput.new(input, output)); + break; + } + } + return result; + }; + return innerFunc; + } + + // BUILDING + async buildCollectSellers( + mapInputs: CollectSellersMapInput, + extra: { sellers: UTxO[]; treasuryRefInput: UTxO }, + ): Promise { + let { sellers, treasuryRefInput } = extra; + this.builder.clean(); + let options: BuildCollectSellersOptions = { + treasuryRefInput, + managerInput: mapInputs.manager[0], + sellerInputs: sellers, + validFrom: Date.now(), + validTo: Date.now() + 3 * 60 * 60 * 1000, + }; + this.builder.tasks.push(() => { + this.builder.tx.collectFrom(mapInputs["seed"]); + }); + this.builder.buildCollectSeller(options); + let txBuilder = this.builder.complete(); + let tx = await txBuilder.complete(); + let signedTx = tx.sign(); + let txSigned = await signedTx.complete(); + return txSigned; + } + + collectSellersChaining(lbeUtxo: LbeUTxO, seed: UTxO): Chaining { + let stopCondition = () => { + return lbeUtxo.sellers.length === 0; + }; + let mapInputs: CollectSellersMapInput = { + seed: [seed], + treasury: [lbeUtxo.treasury], + manager: [lbeUtxo.manager!], + sellers: lbeUtxo.sellers, + }; + let inputIdentifyFuncs: Record = { + seed: this.identifyCommon(seed.address), + manager: this.identifyCommon(this.builder.managerAddress), + }; + let buildTx = async ( + args: Record, + extra: { sellers: UTxO[]; treasuryRefInput: UTxO }, + ) => { + let batchSellers = extra.sellers.splice( + 0, + Number(MINIMUM_SELLER_COLLECTED), + ); + return this.buildCollectSellers(args as CollectSellersMapInput, { + ...extra, + sellers: batchSellers, + }); + }; + return { + mapInputs, + stopCondition, + inputIdentifyFuncs, + buildTx, + submitTx: async (tx: string) => { + this.submitTx(tx); + }, + }; + } + + buildCollectManager(lbeUtxo: LbeUTxO, seed: UTxO): Tx { + this.builder.clean(); + let options: BuildCollectManagerOptions = { + treasuryInput: lbeUtxo.treasury, + managerInput: lbeUtxo.manager!, + validFrom: Date.now(), + validTo: Date.now() + 3 * 60 * 60 * 1000, + }; + this.builder.buildCollectManager(options); + this.builder.tasks.push(() => { + this.builder.tx.collectFrom([seed]); + }); + let tx = this.builder.complete(); + return tx; + } + + async batching() { + await this.buildMapLbeUTxO(); + for (let lbeUtxo of Object.values(this.mapLbeUTxO)) { + let phase = LbePhase.fromLbeUtxo(lbeUtxo); + if (phase) { + console.log(`doing phase ${phase}`); + let seed = ( + await this.builder.t.utxosAt(await this.builder.t.wallet.address()) + )[0]; + let chaining = this.collectSellersChaining(lbeUtxo, seed); + await doChaining(chaining, { + sellers: lbeUtxo.sellers, + treasuryRefInput: lbeUtxo.treasury, + }); + } + } + } + + async buildMapLbeUTxO() { + let things: { + tag: LbeValidator; + address: Address; + token: Unit; + cb: (lbeId: LbeId, utxo: UTxO) => void; // set mapLbeUTxO + }[] = [ + { + tag: "treasury", + address: this.builder.treasuryAddress, + token: this.builder.treasuryToken, + cb: (lbeId, utxo) => { + this.mapLbeUTxO[lbeId] = { + ...(this.mapLbeUTxO[lbeId] ?? {}), + treasury: utxo, + }; + }, + }, + { + tag: "manager", + address: this.builder.managerAddress, + token: this.builder.managerToken, + cb: (lbeId, utxo) => { + this.mapLbeUTxO[lbeId] = { + ...(this.mapLbeUTxO[lbeId] ?? {}), + manager: utxo, + }; + }, + }, + { + tag: "seller", + address: this.builder.sellerAddress, + token: this.builder.sellerToken, + cb: (lbeId, utxo) => { + let sellers = this.mapLbeUTxO[lbeId]?.sellers ?? []; + sellers.push(utxo); + this.mapLbeUTxO[lbeId] = { + ...(this.mapLbeUTxO[lbeId] ?? {}), + sellers, + }; + }, + }, + { + tag: "order", + address: this.builder.orderAddress, + token: this.builder.orderToken, + cb: (lbeId, utxo) => { + let orders = this.mapLbeUTxO[lbeId]?.orders ?? []; + orders.push(utxo); + this.mapLbeUTxO[lbeId] = { + ...(this.mapLbeUTxO[lbeId] ?? {}), + orders, + }; + }, + }, + ]; + for (let { address, tag, token, cb } of things) { + let utxos = await this.builder.t.utxosAtWithUnit(address, token); + for (let utxo of utxos) { + let lbeId = LbeId.fromUtxo({ tag, utxo }); + cb(lbeId, utxo); + } + } + } +} + +// let main = async () => { +// }; + +// main(); diff --git a/src/cmd/hihi.ts b/src/cmd/hihi.ts new file mode 100644 index 0000000..79ad8eb --- /dev/null +++ b/src/cmd/hihi.ts @@ -0,0 +1,141 @@ +import * as T from "@minswap/translucent"; + +import type { + CTransactionHash, + CTransactionOutputs, + CTransactionUnspentOutputs, + MaestroSupportedNetworks, + TxSigned, + UTxO, +} from "../types"; + +type InputId = string; +type InputIdentify = ( + txHash: CTransactionHash, + finalOutputs: CTransactionOutputs, +) => CTransactionUnspentOutputs; + +let submitTx = async (tx: string): Promise => { + console.log({ tx }); + const response = await fetch("https://dev-3.minswap.org/graphql", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: `{"query":"mutation SubmitTx($tx: String!) {submitTx(tx: $tx)}","variables":{"tx":"${tx}"}}`, + }); + const result = await response.text(); + return result; +}; + +type Chaining = { + mapInputs: Record; + inputIdentifyFuncs: Record; + buildTx: (mapInputs: Record) => Promise; + stopCondition: () => boolean; +}; + +let doChaining = async (options: Chaining) => { + let { mapInputs, inputIdentifyFuncs, buildTx, stopCondition } = options; + let C = T.CModuleLoader.get; + while (true) { + console.log("mapInputs", mapInputs); + if (stopCondition()) { + return; + } + let tx = await buildTx(mapInputs); + let debugTxHash = await submitTx(tx.toString()); + let transactionHash = tx.toHash(); + let txHash = C.TransactionHash.from_hex(transactionHash); + console.log({ + "debugTxHash ": debugTxHash, + "transactionHash ": transactionHash, + "txHash ": txHash.to_hex(), + }); + let finalOutputs = tx.txSigned.body().outputs(); + for (let [k, v] of Object.entries(inputIdentifyFuncs)) { + let coreUtxos = v(txHash, finalOutputs); + let utxos: UTxO[] = []; + for (let i = 0; i < coreUtxos.len(); i++) { + let coreUtxo = coreUtxos.get(i); + utxos.push(T.coreToUtxo(coreUtxo)); + } + mapInputs[k] = utxos; + } + } +}; + +let main = async () => { + await T.loadModule(); + await T.CModuleLoader.load(); + let network: MaestroSupportedNetworks = "Preprod"; + let maestroApiKey = "E0n5jUy4j40nhKCuB7LrYabTNieG0egu" ?? process.argv[2]; + let seedPhase = + "muffin spell cement resemble frame pupil grow gloom hawk wild item hungry polar ice maximum sport economy drop sun timber stone circle army jazz" ?? + process.argv[3]; + + const maestro = new T.Maestro({ network, apiKey: maestroApiKey }); + const t = await T.Translucent.new(maestro, network); + t.selectWalletFromSeed(seedPhase); + + let address = await t.wallet.address(); + let C = T.CModuleLoader.get; + + let inputIdentifyFuncs = { + root: (txHash: CTransactionHash, finalOutputs: CTransactionOutputs) => { + console.log("finalOutputs", finalOutputs.to_js_value()); + let result = C.TransactionUnspentOutputs.new(); + for (let i = 0; i <= finalOutputs.len(); i++) { + let output = finalOutputs.get(i); + console.log("output", output.to_js_value()); + let curAddress = output.address(); + if ( + curAddress.to_bech32() === address && + BigInt(output.amount().coin().to_str()) >= 500_000_000n + ) { + let input = C.TransactionInput.new( + txHash, + C.BigNum.from_str(i.toString()), + ); + result.add(C.TransactionUnspentOutput.new(input, output)); + break; + } + } + return result; + }, + }; + + let count = 0; + let stopCondition = () => { + console.log({ count }); + return count++ >= 4; + }; + + let mapInputs = { + root: (await t.utxosAt(address)).filter( + (u) => u.assets["lovelace"] >= 500_000_000n, + ), + }; + let buildTx = async ( + mapInputs: Record, + ): Promise => { + let txBuilder = t.newTx(); + txBuilder + .collectFrom(mapInputs["root"]) + .payToAddress(address, { lovelace: 3_000_000n }); + let tx = await txBuilder.complete(); + let signedTx = tx.sign(); + let txSigned = await signedTx.complete(); + console.log("txSigned", txSigned.txSigned.to_js_value()); + return txSigned; + }; + + let options: Chaining = { + mapInputs, + inputIdentifyFuncs, + buildTx, + stopCondition, + }; + await doChaining(options); +}; +main(); diff --git a/src/constants.ts b/src/constants.ts index 08b0d9a..8e526ec 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -25,3 +25,10 @@ export const MAX_PENALTY_RATE = 25n; export const MINIMUM_SELLER_COLLECTED = 20n; export const MINIMUM_ORDER_COLLECTED = 30n; export const MINIMUM_ORDER_REDEEMED = 30n; + +// Batching +export const MAX_COLLECT_SELLERS = 30; +export const MAX_COLLECT_ORDERS = 50; +export const MAX_REDEEM_ORDERS = 50; +export const MAX_REFUND_ORDERS = 50; +export const MAX_SEEDS = 100; diff --git a/src/tests/batcher.test.ts b/src/tests/batcher.test.ts new file mode 100644 index 0000000..4bbc9ef --- /dev/null +++ b/src/tests/batcher.test.ts @@ -0,0 +1,91 @@ +import * as T from "@minswap/translucent"; +import { WarehouseBuilder } from "../build-tx"; +import { WarehouseBatcher } from "../cmd/batcher"; +import type { ManagerDatum, TreasuryDatum } from "../types"; +import { loadModule } from "./utils"; +import { + genWarehouse, + skipToCountingPhase, + type GenWarehouse, +} from "./warehouse"; + +let warehouse: GenWarehouse; +let defaultMaxTxSize = T.PROTOCOL_PARAMETERS_DEFAULT.maxTxSize; + +const SELLER_COUNT = 3; + +beforeAll(async () => { + await loadModule(); +}); + +beforeEach(async () => { + warehouse = await genWarehouse(defaultMaxTxSize); +}); + +test("batcher | buildMapLbeUTxO", async () => { + let { builder, emulator } = warehouse; + + emulator.addUTxO(warehouse.defaultTreasuryInput); + emulator.addUTxO(warehouse.defaultManagerInput); + emulator.addUTxO(warehouse.defaultSellerInput); + emulator.addUTxO(warehouse.defaultOrderInput); + + let expectMapLbeUTxO = { + treasury: warehouse.defaultTreasuryInput, + manager: warehouse.defaultManagerInput, + sellers: [warehouse.defaultSellerInput], + orders: [warehouse.defaultOrderInput], + }; + let batcher = new WarehouseBatcher(builder); + await batcher.buildMapLbeUTxO(); + + expect(Object.keys(batcher.mapLbeUTxO).length).toEqual(1); + let mapLbeUTxO = Object.values(batcher.mapLbeUTxO)[0]; + expect(mapLbeUTxO).toEqual(expectMapLbeUTxO); +}); + +test("batcher | collect-sellers", async () => { + let { builder, emulator } = warehouse; + // IMPORTANT!!! Update LBE End Time + let treasuryDatum: TreasuryDatum = { + ...warehouse.defaultTreasuryDatum, + startTime: BigInt(Date.now() - 3 * 24 * 60 * 60 * 1000), + endTime: BigInt(Date.now() - 60 * 60 * 1000), + }; + let treasuryInput = { + ...warehouse.defaultTreasuryInput, + datum: WarehouseBuilder.toDatumTreasury(treasuryDatum), + }; + let managerDatum: ManagerDatum = { + ...warehouse.defaultManagerDatum, + sellerCount: BigInt(SELLER_COUNT), + }; + let managerInput = { + ...warehouse.defaultManagerInput, + datum: WarehouseBuilder.toDatumManager(managerDatum), + }; + let sellerInputs: T.UTxO[] = []; + for (let i = 0; i < SELLER_COUNT; i++) { + let utxo: T.UTxO = { + ...warehouse.defaultSellerInput, + outputIndex: warehouse.outputIndex++, + }; + sellerInputs.push(utxo); + } + + emulator.addUTxO(treasuryInput); + emulator.addUTxO(managerInput); + for (let s of sellerInputs) emulator.addUTxO(s); + skipToCountingPhase({ + t: builder.t, + e: emulator, + datum: treasuryDatum, + }); + + let batcher = new WarehouseBatcher(builder); + await batcher.batching(); + + // collect all sellers => Seller Address doesn't have any UTxO + let sellers = await warehouse.findSellers(); + expect(sellers.length).toEqual(0); +}); diff --git a/src/tests/close-event.test.ts b/src/tests/close-event.test.ts index 8a385ec..fe8fc42 100644 --- a/src/tests/close-event.test.ts +++ b/src/tests/close-event.test.ts @@ -25,7 +25,7 @@ beforeEach(async () => { }; let treasuryInput: UTxO = { address: builder.treasuryAddress, - datum: builder.toDatumTreasury(treasuryDatum), + datum: WarehouseBuilder.toDatumTreasury(treasuryDatum), assets: { [builder.treasuryToken]: 1n, lovelace: TREASURY_MIN_ADA, @@ -56,7 +56,7 @@ beforeEach(async () => { [builder.factoryToken]: 1n, }, address: builder.factoryAddress, - datum: builder.toDatumFactory(factoryHeadDatum), + datum: WarehouseBuilder.toDatumFactory(factoryHeadDatum), }, { txHash: "00".repeat(32), @@ -67,7 +67,7 @@ beforeEach(async () => { [builder.factoryToken]: 1n, }, address: builder.factoryAddress, - datum: builder.toDatumFactory(factoryTailDatum), + datum: WarehouseBuilder.toDatumFactory(factoryTailDatum), }, ]; let options: BuildCloseEventOptions = { diff --git a/src/tests/collect-manager.test.ts b/src/tests/collect-manager.test.ts index cab2199..51a97ed 100644 --- a/src/tests/collect-manager.test.ts +++ b/src/tests/collect-manager.test.ts @@ -27,7 +27,7 @@ beforeEach(async () => { lovelace: 2_000_000n, }, address: builder.managerAddress, - datum: builder.toDatumManager(managerDatum), + datum: WarehouseBuilder.toDatumManager(managerDatum), }; const treasuryDatum: TreasuryDatum = { ...W.defaultTreasuryDatum, @@ -41,7 +41,7 @@ beforeEach(async () => { lovelace: TREASURY_MIN_ADA, }, address: builder.treasuryAddress, - datum: builder.toDatumTreasury(treasuryDatum), + datum: WarehouseBuilder.toDatumTreasury(treasuryDatum), }; const options: BuildCollectManagerOptions = { treasuryInput: treasuryInput, @@ -111,7 +111,7 @@ test("collect-manager | FAIL | LBE ID missmatch", async () => { }; const treasuryInput: UTxO = { ...W.treasuryInput, - datum: builder.toDatumTreasury(treasuryInDatum), + datum: WarehouseBuilder.toDatumTreasury(treasuryInDatum), }; const options = { ...W.options, diff --git a/src/tests/collect-orders.test.ts b/src/tests/collect-orders.test.ts index c9ae4aa..e2e85fe 100644 --- a/src/tests/collect-orders.test.ts +++ b/src/tests/collect-orders.test.ts @@ -40,7 +40,7 @@ async function genTestWarehouse() { lovelace: TREASURY_MIN_ADA, }, address: builder.treasuryAddress, - datum: builder.toDatumTreasury(treasuryDatum), + datum: WarehouseBuilder.toDatumTreasury(treasuryDatum), }; let orderDatum: OrderDatum = { ...warehouse.defaultOrderDatum, @@ -55,7 +55,7 @@ async function genTestWarehouse() { lovelace: ORDER_MIN_ADA + 2n * ORDER_COMMISSION + orderAmount, }, address: builder.orderAddress, - datum: builder.toDatumOrder(orderDatum), + datum: WarehouseBuilder.toDatumOrder(orderDatum), }; const options: BuildCollectOrdersOptions = { treasuryInput: treasuryInput, @@ -82,7 +82,7 @@ let remixTreasuryInput = (remixDatum: any): UTxO => { }; return { ...W.treasuryInput, - datum: W.builder.toDatumTreasury(treasuryDatum), + datum: WarehouseBuilder.toDatumTreasury(treasuryDatum), }; }; diff --git a/src/tests/collect-sellers.test.ts b/src/tests/collect-sellers.test.ts index c7dc166..6f9d89e 100644 --- a/src/tests/collect-sellers.test.ts +++ b/src/tests/collect-sellers.test.ts @@ -93,7 +93,7 @@ async function genTestWarehouse() { treasuryDatum.reserveBase, }, address: builder.treasuryAddress, - datum: builder.toDatumTreasury(treasuryDatum), + datum: WarehouseBuilder.toDatumTreasury(treasuryDatum), }; const managerDatum: ManagerDatum = { ...defaultManagerDatum, @@ -107,7 +107,7 @@ async function genTestWarehouse() { lovelace: MANAGER_MIN_ADA, }, address: builder.managerAddress, - datum: builder.toDatumManager(managerDatum), + datum: WarehouseBuilder.toDatumManager(managerDatum), }; const sellerDatums: SellerDatum[] = [ { @@ -175,7 +175,7 @@ function genSellerUTxO(datum: SellerDatum, builder: WarehouseBuilder): UTxO { lovelace: SELLER_MIN_ADA + BigInt(rand) * SELLER_COMMISSION, }, address: builder.sellerAddress, - datum: builder.toDatumSeller(datum), + datum: WarehouseBuilder.toDatumSeller(datum), }; } @@ -213,7 +213,10 @@ test("collect sellers | PASS | happy case 2", async () => { ); warehouse.options.managerInput = { ...warehouse.options.managerInput, - datum: builder.toDatumManager({ ...managerDatum, sellerCount: 4n }), + datum: WarehouseBuilder.toDatumManager({ + ...managerDatum, + sellerCount: 4n, + }), }; const builder1 = stupidManagerDatumOut({ reserveRaise: warehouse.expectedManagerDatumOut.reserveRaise + 1000n, @@ -228,12 +231,12 @@ test("collect sellers | PASS | happy case 3", async () => { // collect MINIMUM_SELLER_COLLECTED sellers let { builder, W, defaultSellerDatum, options, treasuryDatum } = warehouse; let managerDatum: ManagerDatum = { - ...builder.fromDatumManager(options.managerInput.datum!), + ...WarehouseBuilder.fromDatumManager(options.managerInput.datum!), sellerCount: 100n, }; let managerInput = { ...options.managerInput, - datum: builder.toDatumManager(managerDatum), + datum: WarehouseBuilder.toDatumManager(managerDatum), }; let sellerInputs: UTxO[] = []; for (let i = 0; i <= MINIMUM_SELLER_COLLECTED; i++) { @@ -294,7 +297,7 @@ test("collect sellers | FAIL | No treasury ref input", async () => { .readFrom([builder.deployedValidators["sellerValidator"]]) .collectFrom( builder.sellerInputs, - builder.toRedeemerSellerSpend(builder.sellerRedeemer), + WarehouseBuilder.toRedeemerSellerSpend(builder.sellerRedeemer), ); }; assertValidatorFail(builder); @@ -312,7 +315,7 @@ test("collect sellers | FAIL | No treasury ref input", async () => { .readFrom([builder.deployedValidators["sellerValidator"]]) .collectFrom( builder.sellerInputs, - builder.toRedeemerSellerSpend(builder.sellerRedeemer), + WarehouseBuilder.toRedeemerSellerSpend(builder.sellerRedeemer), ); }; assertValidatorFail(builder); @@ -420,7 +423,7 @@ test("collect sellers | FAIL | Seller Input: Invalid LBE ID", async () => { const { sellerInputs } = options; sellerInputs[0] = { ...sellerInputs[0], - datum: builder.toDatumSeller({ + datum: WarehouseBuilder.toDatumSeller({ ...sellerDatums[0], baseAsset: MINt, }), @@ -440,7 +443,10 @@ test("collect sellers | FAIL | Manager Input: Invalid LBE ID", async () => { ...options, managerInput: { ...managerInput, - datum: builder.toDatumManager({ ...managerDatum, baseAsset: MINt }), + datum: WarehouseBuilder.toDatumManager({ + ...managerDatum, + baseAsset: MINt, + }), }, }); assertValidatorFail(builder); @@ -468,12 +474,12 @@ test("collect sellers | FAIL | More than 1 manager input", async () => { test("collect sellers | FAIL | spam collect sellers", async () => { let { builder, options } = warehouse; let managerDatum: ManagerDatum = { - ...builder.fromDatumManager(options.managerInput.datum!), + ...WarehouseBuilder.fromDatumManager(options.managerInput.datum!), sellerCount: 100n, }; let managerInput = { ...options.managerInput, - datum: builder.toDatumManager(managerDatum), + datum: WarehouseBuilder.toDatumManager(managerDatum), }; options = { ...options, diff --git a/src/tests/create-amm-pool.test.ts b/src/tests/create-amm-pool.test.ts index 76afdb4..d7f4fea 100644 --- a/src/tests/create-amm-pool.test.ts +++ b/src/tests/create-amm-pool.test.ts @@ -73,7 +73,7 @@ async function genTestWarehouse() { treasuryDatum.reserveBase, }, address: builder.treasuryAddress, - datum: builder.toDatumTreasury(treasuryDatum), + datum: WarehouseBuilder.toDatumTreasury(treasuryDatum), }; const ammFactoryDatum: FactoryValidatorValidateFactory["datum"] = { @@ -157,7 +157,7 @@ async function buildTxWithStupidTreasuryDatum( treasuryDatum.reserveBase, }, address: builder.treasuryAddress, - datum: builder.toDatumTreasury(treasuryDatum), + datum: WarehouseBuilder.toDatumTreasury(treasuryDatum), }; builder.buildCreateAmmPool({ ...options, treasuryInput: treasuryUTxO }); await assertValidatorFail(builder); @@ -221,7 +221,7 @@ test("Create AMM Pool | FAIL | Invalid Treasury out value", async () => { builder.tx.payToAddressWithData( builder.treasuryAddress, { - inline: builder.toDatumTreasury(treasuryOutDatum), + inline: WarehouseBuilder.toDatumTreasury(treasuryOutDatum), }, createPoolAssets(), ); diff --git a/src/tests/create-treasury.test.ts b/src/tests/create-treasury.test.ts index ffb44b6..bcb3a10 100644 --- a/src/tests/create-treasury.test.ts +++ b/src/tests/create-treasury.test.ts @@ -34,7 +34,7 @@ beforeEach(async () => { lovelace: 2_000_000n, [builder.factoryToken]: 1n, }, - datum: builder.toDatumFactory(defaultFactoryDatum), + datum: WarehouseBuilder.toDatumFactory(defaultFactoryDatum), address: builder.factoryAddress, }; const owner = await t.wallet.address(); @@ -87,7 +87,7 @@ test("create-treasury | PASS | Penalty Config", async () => { test("create-treasury | PASS | Receiver Datum: DatumHash", async () => { let { defaultTreasuryDatum, options } = W; let builder: WarehouseBuilder = W.builder; - let extraDatum = builder.toDatumFactory({ head: "00", tail: "ff" }); + let extraDatum = WarehouseBuilder.toDatumFactory({ head: "00", tail: "ff" }); let extraDatumHash = builder.t.utils.datumToHash(extraDatum); let treasuryDatum: TreasuryDatum = { ...defaultTreasuryDatum, @@ -105,7 +105,7 @@ test("create-treasury | PASS | Receiver Datum: DatumHash", async () => { test("create-treasury | PASS | Receiver Datum: InlineDatum", async () => { let { defaultTreasuryDatum, options } = W; let builder: WarehouseBuilder = W.builder; - let extraDatum = builder.toDatumFactory({ head: "00", tail: "ff" }); + let extraDatum = WarehouseBuilder.toDatumFactory({ head: "00", tail: "ff" }); let extraDatumHash = builder.t.utils.datumToHash(extraDatum); let treasuryDatum: TreasuryDatum = { ...defaultTreasuryDatum, @@ -147,7 +147,7 @@ test("create-treasury | FAIL | have 2 Factory Inputs", async () => { builder.tasks.push(() => { builder.tx.collectFrom( [dummyFactoryInput], - builder.toRedeemerFactory(builder.factoryRedeemer), + WarehouseBuilder.toRedeemerFactory(builder.factoryRedeemer), ); }); assertValidatorFail(txBuilder); @@ -168,7 +168,7 @@ test("create-treasury | FAIL | have 3 Factory Outs", async () => { builder.tx.payToAddressWithData( builder.factoryAddress, { - inline: builder.toDatumFactory({ head: "00", tail: "ff" }), + inline: WarehouseBuilder.toDatumFactory({ head: "00", tail: "ff" }), }, { lovelace: 1_000_000n, @@ -183,7 +183,7 @@ test("create-treasury | FAIL | Factory Out Tail Datum incorrect!", async () => { let builder: WarehouseBuilder = W.builder; builder = builder.buildCreateTreasury(W.options); builder.tasks[1] = () => { - const factoryDatum = builder.fromDatumFactory( + const factoryDatum = WarehouseBuilder.fromDatumFactory( builder.factoryInputs[0].datum!, ); const newFactoryHeadDatum = { @@ -206,7 +206,7 @@ test("create-treasury | FAIL | Factory Out Head Datum incorrect!", async () => { let builder: WarehouseBuilder = W.builder; builder = builder.buildCreateTreasury(W.options); builder.tasks[1] = () => { - const factoryDatum = builder.fromDatumFactory( + const factoryDatum = WarehouseBuilder.fromDatumFactory( builder.factoryInputs[0].datum!, ); const newFactoryHeadDatum = { @@ -232,7 +232,7 @@ const remixManagerDatum = (remixDatum: any) => { builder.tx.payToAddressWithData( builder.managerAddress, { - inline: builder.toDatumManager({ + inline: WarehouseBuilder.toDatumManager({ ...W.defaultManagerDatum, ...remixDatum, }), @@ -266,7 +266,7 @@ const remixTreasuryValue = (assets: Assets) => { builder.tx.payToAddressWithData( builder.treasuryAddress, { - inline: builder.toDatumTreasury(W.defaultTreasuryDatum), + inline: WarehouseBuilder.toDatumTreasury(W.defaultTreasuryDatum), }, assets, ); diff --git a/src/tests/example.test.ts b/src/tests/example.test.ts index f2c46bf..4cd20f2 100644 --- a/src/tests/example.test.ts +++ b/src/tests/example.test.ts @@ -151,13 +151,7 @@ beforeEach(async () => { ammDeployedValidators = await deployMinswapValidators(t, ammValidators); // registerStake - await quickSubmitBuilder(emulator)({ - txBuilder: t - .newTx() - .registerStake( - t.utils.validatorToRewardAddress(validators.sellerValidator), - ), - }); + // Need to register Stake Factory Validator because it supports WithdrawFrom await quickSubmitBuilder(emulator)({ txBuilder: t .newTx() @@ -189,7 +183,7 @@ test("example flow", async () => { // create treasury const discoveryStartSlot = emulator.slot + 60 * 60; const discoveryEndSlot = discoveryStartSlot + 60 * 60 * 24 * 2; // 2 days - extraDatum = builder.toDatumFactory({ head: "00", tail: "ff" }); // dummy + extraDatum = WarehouseBuilder.toDatumFactory({ head: "00", tail: "ff" }); // dummy let extraDatumHash = t.utils.datumToHash(extraDatum); const treasuryDatum: TreasuryDatum = { factoryPolicyId: t.utils.validatorToScriptHash(validators.factoryValidator), @@ -435,7 +429,7 @@ test("example flow", async () => { ) .filter((u) => !u.scriptRef) .filter((u) => { - const datum = builder.fromDatumOrder(u.datum!); + const datum = WarehouseBuilder.fromDatumOrder(u.datum!); return datum.isCollected == false; }) as UTxO[]; maxCount = maxCount ?? orderUtxos.length; diff --git a/src/tests/init-factory.test.ts b/src/tests/init-factory.test.ts index 7fbba35..e55189f 100644 --- a/src/tests/init-factory.test.ts +++ b/src/tests/init-factory.test.ts @@ -94,7 +94,7 @@ test("init-factory | FAIL | missing Factory Token", async () => { builder.tx.payToAddressWithData( builder.factoryAddress, { - inline: builder.toDatumFactory(factoryDatum), + inline: WarehouseBuilder.toDatumFactory(factoryDatum), }, { lovelace: 2_000_000n, @@ -117,7 +117,7 @@ test("init-factory | FAIL | Factory Datum is not correct", async () => { builder.tx.payToAddressWithData( builder.factoryAddress, { - inline: builder.toDatumFactory(factoryDatum), + inline: WarehouseBuilder.toDatumFactory(factoryDatum), }, { [builder.factoryToken]: 1n, diff --git a/src/tests/performance.test.ts b/src/tests/performance.test.ts index 74aa474..2a19afb 100644 --- a/src/tests/performance.test.ts +++ b/src/tests/performance.test.ts @@ -1,8 +1,9 @@ import * as T from "@minswap/translucent"; -import type { - BuildCollectOrdersOptions, - BuildCollectSellersOptions, - BuildRedeemOrdersOptions, +import { + WarehouseBuilder, + type BuildCollectOrdersOptions, + type BuildCollectSellersOptions, + type BuildRedeemOrdersOptions, } from "../build-tx"; import type { Assets, @@ -44,7 +45,7 @@ test("performance | collect-seller", async () => { }; let managerInput = { ...warehouse.defaultManagerInput, - datum: builder.toDatumManager(managerDatum), + datum: WarehouseBuilder.toDatumManager(managerDatum), }; let sellerInputs: UTxO[] = []; for (let i = 0; i < MAX_COLLECT_SELLER_COUNT; i++) { @@ -98,7 +99,7 @@ test("performance | collect-order", async () => { }; let treasuryInput: UTxO = { ...warehouse.defaultTreasuryInput, - datum: builder.toDatumTreasury(treasuryDatum), + datum: WarehouseBuilder.toDatumTreasury(treasuryDatum), }; let options: BuildCollectOrdersOptions = { validFrom, @@ -137,7 +138,7 @@ test("performance | refund-order", async () => { let utxo: UTxO = { ...warehouse.defaultOrderInput, outputIndex: warehouse.outputIndex++, - datum: builder.toDatumOrder(orderDatum), + datum: WarehouseBuilder.toDatumOrder(orderDatum), }; orderInputs.push(utxo); collectedFund += orderDatum.amount + orderDatum.penaltyAmount; @@ -157,7 +158,7 @@ test("performance | refund-order", async () => { let treasuryAssets = warehouse.defaultTreasuryInput.assets; let treasuryInput: UTxO = { ...warehouse.defaultTreasuryInput, - datum: builder.toDatumTreasury(treasuryDatum), + datum: WarehouseBuilder.toDatumTreasury(treasuryDatum), assets: { ...treasuryAssets, [raiseAsset]: (treasuryAssets[raiseAsset] ?? 0n) + collectedFund, @@ -196,7 +197,7 @@ test("performance | redeem-order", async () => { let utxo: UTxO = { ...warehouse.defaultOrderInput, outputIndex: warehouse.outputIndex++, - datum: builder.toDatumOrder(orderDatum), + datum: WarehouseBuilder.toDatumOrder(orderDatum), }; orderInputs.push(utxo); collectedFund += orderDatum.amount + orderDatum.penaltyAmount; @@ -226,7 +227,7 @@ test("performance | redeem-order", async () => { }; let treasuryInput: UTxO = { ...warehouse.defaultTreasuryInput, - datum: builder.toDatumTreasury(treasuryDatum), + datum: WarehouseBuilder.toDatumTreasury(treasuryDatum), assets: treasuryAssets, }; let options: BuildRedeemOrdersOptions = { diff --git a/src/tests/redeem-lp.test.ts b/src/tests/redeem-lp.test.ts index d785671..aee6ac5 100644 --- a/src/tests/redeem-lp.test.ts +++ b/src/tests/redeem-lp.test.ts @@ -71,7 +71,7 @@ async function genTestWarehouse() { [builder.ammLpToken!]: totalLiquidity, }, address: builder.treasuryAddress, - datum: builder.toDatumTreasury(treasuryDatum), + datum: WarehouseBuilder.toDatumTreasury(treasuryDatum), }; const orderInDatums: OrderDatum[] = [ { @@ -116,7 +116,7 @@ async function genTestWarehouse() { : 0n; const raiseAssetUnit = toUnit(raiseAsset.policyId, raiseAsset.assetName); for (const order of sortedOrders) { - const datum = builder.fromDatumOrder(order.datum!); + const datum = WarehouseBuilder.fromDatumOrder(order.datum!); const lpAmount = (datum.amount * treasuryDatum.totalLiquidity) / treasuryDatum.reserveRaise; @@ -165,7 +165,7 @@ function genOrderUTxO(datum: OrderDatum, builder: WarehouseBuilder): UTxO { lovelace: ORDER_MIN_ADA + ORDER_COMMISSION, }, address: builder.orderAddress, - datum: builder.toDatumOrder(datum), + datum: WarehouseBuilder.toDatumOrder(datum), }; } @@ -190,6 +190,9 @@ function attachValueToInput(value: Assets): void { ]); }); } + +test("Redeem LP | PASS | penalty orders", async () => {}); + test("Redeem LP | FAIL | No treasury input", async () => { const { builder, options, treasuryUTxO } = warehouse; builder.buildRedeemOrders(options); @@ -223,7 +226,10 @@ test("Redeem LP | FAIL | Not created pool yet", async () => { options.treasuryInput = { ...options.treasuryInput, assets: { ...options.treasuryInput.assets, [builder.ammLpToken!]: 0n }, - datum: builder.toDatumTreasury({ ...treasuryDatum, totalLiquidity: 0n }), + datum: WarehouseBuilder.toDatumTreasury({ + ...treasuryDatum, + totalLiquidity: 0n, + }), }; builder.buildRefundOrders(options); assertValidatorFail(builder); diff --git a/src/tests/refund.test.ts b/src/tests/refund.test.ts index 5fd6991..8011105 100644 --- a/src/tests/refund.test.ts +++ b/src/tests/refund.test.ts @@ -70,7 +70,7 @@ async function genTestWarehouse() { treasuryDatum.reserveBase, }, address: builder.treasuryAddress, - datum: builder.toDatumTreasury(treasuryDatum), + datum: WarehouseBuilder.toDatumTreasury(treasuryDatum), }; const orderInDatums: OrderDatum[] = [ { @@ -106,7 +106,7 @@ async function genTestWarehouse() { const raiseAssetUnit = toUnit(raiseAsset.policyId, raiseAsset.assetName); const sortedOrders = sortUTxOs(orderInputUTxOs); for (const order of sortedOrders) { - const { penaltyAmount, amount, owner } = builder.fromDatumOrder( + const { penaltyAmount, amount, owner } = WarehouseBuilder.fromDatumOrder( order.datum!, ); const assets: Record = { @@ -151,7 +151,7 @@ function genOrderUTxO(datum: OrderDatum, builder: WarehouseBuilder): UTxO { lovelace: ORDER_MIN_ADA + ORDER_COMMISSION, }, address: builder.orderAddress, - datum: builder.toDatumOrder(datum), + datum: WarehouseBuilder.toDatumOrder(datum), }; } @@ -207,7 +207,7 @@ test("Refund | FAIL | Not collected manager", async () => { const { builder, options, treasuryDatum } = warehouse; options.treasuryInput = { ...options.treasuryInput, - datum: builder.toDatumTreasury({ + datum: WarehouseBuilder.toDatumTreasury({ ...treasuryDatum, isManagerCollected: false, }), @@ -219,7 +219,7 @@ test("Refund | FAIL | Not collected all orders", async () => { const { builder, options, treasuryDatum } = warehouse; options.treasuryInput = { ...options.treasuryInput, - datum: builder.toDatumTreasury({ + datum: WarehouseBuilder.toDatumTreasury({ ...treasuryDatum, collectedFund: treasuryDatum.collectedFund - 10n, }), @@ -231,7 +231,7 @@ test("Refund | FAIL | Not collected all orders", async () => { const { builder, options, treasuryDatum } = warehouse; options.treasuryInput = { ...options.treasuryInput, - datum: builder.toDatumTreasury({ + datum: WarehouseBuilder.toDatumTreasury({ ...treasuryDatum, isCancelled: false, }), diff --git a/src/tests/update-event.test.ts b/src/tests/update-event.test.ts index 26868d4..e2a171a 100644 --- a/src/tests/update-event.test.ts +++ b/src/tests/update-event.test.ts @@ -25,7 +25,7 @@ async function genTestWarehouse() { lovelace: TREASURY_MIN_ADA + CREATE_POOL_COMMISSION, }, address: builder.treasuryAddress, - datum: builder.toDatumTreasury(treasuryDatum), + datum: WarehouseBuilder.toDatumTreasury(treasuryDatum), }; let options: BuildUpdateLBEOptions = { treasuryInput, @@ -67,7 +67,7 @@ test("Update LBE | FAIL | update when LBE is cancelled", async () => { }; treasuryInput = { ...treasuryInput, - datum: builder.toDatumTreasury(treasuryDatum), + datum: WarehouseBuilder.toDatumTreasury(treasuryDatum), }; options = { ...options, diff --git a/src/tests/using-seller.test.ts b/src/tests/using-seller.test.ts index 63a09c9..ed2936d 100644 --- a/src/tests/using-seller.test.ts +++ b/src/tests/using-seller.test.ts @@ -91,7 +91,7 @@ async function genTestWarehouse() { treasuryDatum.reserveBase, }, address: builder.treasuryAddress, - datum: builder.toDatumTreasury(treasuryDatum), + datum: WarehouseBuilder.toDatumTreasury(treasuryDatum), }; const sellerDatum = { ...defaultSellerDatum, @@ -106,7 +106,7 @@ async function genTestWarehouse() { lovelace: SELLER_MIN_ADA + SELLER_COMMISSION * 5n, }, address: builder.sellerAddress, - datum: builder.toDatumSeller(sellerDatum), + datum: WarehouseBuilder.toDatumSeller(sellerDatum), }; const orderInDatums: OrderDatum[] = [ { @@ -190,7 +190,7 @@ function genOrderUTxO(datum: OrderDatum, builder: WarehouseBuilder): UTxO { datum.penaltyAmount, }, address: builder.orderAddress, - datum: builder.toDatumOrder(datum), + datum: WarehouseBuilder.toDatumOrder(datum), }; } @@ -242,7 +242,10 @@ test("using-seller | FAIL | update orders: LBE is cancelled", async () => { ...warehouse.options, treasuryRefInput: { ...treasuryUTxO, - datum: builder.toDatumTreasury({ ...treasuryDatum, isCancelled: true }), + datum: WarehouseBuilder.toDatumTreasury({ + ...treasuryDatum, + isCancelled: true, + }), }, }; builder.buildUsingSeller(options); @@ -321,7 +324,7 @@ test("using-seller | FAIL | update orders: Seller output don't have any seller t builder.tx.payToAddressWithData( builder.sellerAddress, { - inline: builder.toDatumSeller({ + inline: WarehouseBuilder.toDatumSeller({ ...sellerDatum, // new seller output datum amount: -1745n, @@ -348,7 +351,7 @@ test("using-seller | FAIL | update orders: Invalid order output value", async () builder.tx.payToAddressWithData( builder.orderAddress, { - inline: builder.toDatumOrder(datum), + inline: WarehouseBuilder.toDatumOrder(datum), }, assets, ); @@ -366,7 +369,7 @@ test("using-seller | FAIL | update orders: penalty_amount is less than 0", async test("using-seller | FAIL | update orders: Order's input LBE ID miss match", async () => { const { builder, options, orderInDatums } = warehouse; - options.orderInputs[0].datum = builder.toDatumOrder({ + options.orderInputs[0].datum = WarehouseBuilder.toDatumOrder({ ...orderInDatums[0], // MINt baseAsset: MINt, @@ -388,7 +391,7 @@ test("using-seller | FAIL | update orders: Order's output LBE ID miss match", as test("using-seller | FAIL | update orders: Seller's input LBE ID miss match", async () => { const { builder, options, sellerDatum } = warehouse; - options.sellerUtxo.datum = builder.toDatumSeller({ + options.sellerUtxo.datum = WarehouseBuilder.toDatumSeller({ ...sellerDatum, // MINt baseAsset: MINt, diff --git a/src/tests/utils.ts b/src/tests/utils.ts index e73384f..8a36256 100644 --- a/src/tests/utils.ts +++ b/src/tests/utils.ts @@ -111,8 +111,8 @@ export const loadModule = async () => { }; export const DUMMY_SEED_UTXO: UTxO = { - txHash: "5428517bd92102ce1af705f8b66560d445e620aead488b47fb824426484912f8", - outputIndex: 0, + txHash: params.seedOutRef.txHash, + outputIndex: params.seedOutRef.outputIndex, assets: { lovelace: 5_000_000n }, address: "addr_test1vqtx6ahdpzm0nm9qa4wh4avhze8gv3j6jv6v3gmrukdxfrqm6m8d3", }; @@ -123,7 +123,13 @@ export const DUMMY_SEED_AMM_UTXO: UTxO = { assets: { lovelace: 5_000_000n }, address: "addr_test1vqtx6ahdpzm0nm9qa4wh4avhze8gv3j6jv6v3gmrukdxfrqm6m8d3", }; - +export function hexToUtxo(hexUtxo: string): UTxO { + let C = T.CModuleLoader.get; + let cUtxo = C.TransactionUnspentOutput.from_bytes(T.fromHex(hexUtxo)); + let utxo = T.coreToUtxo(cUtxo); + cUtxo.free(); + return utxo; +} const genAmmValidators = async (t: Translucent) => { (t.provider as Emulator).addUTxO(DUMMY_SEED_AMM_UTXO); const ammValidators = collectMinswapValidators({ diff --git a/src/tests/warehouse.ts b/src/tests/warehouse.ts index e060fbc..d7ff174 100644 --- a/src/tests/warehouse.ts +++ b/src/tests/warehouse.ts @@ -67,11 +67,12 @@ export const genWarehouse = async (maxTxSize?: number) => { }; const emulator = new T.Emulator([ACCOUNT_0], protocolParameters); let t = await T.Translucent.new(emulator); - emulator.awaitBlock(10_000); // For validity ranges to be valid + // emulator.awaitBlock(10_000); // For validity ranges to be valid t.selectWalletFromPrivateKey(ACCOUNT_0.privateKey); const warehouseOptions = await genWarehouseOptions(t); // registerStake + // Need to register Stake Factory Validator because it supports WithdrawFrom await quickSubmitBuilder(emulator)({ txBuilder: t .newTx() @@ -167,7 +168,7 @@ export const genWarehouse = async (maxTxSize?: number) => { assets: { [builder.ammPoolToken]: 1n, }, - datum: builder.toDatumAmmPool(ammPoolDatum), + datum: WarehouseBuilder.toDatumAmmPool(ammPoolDatum), txHash: "01".repeat(32), outputIndex: outputIndex++, }; @@ -178,6 +179,13 @@ export const genWarehouse = async (maxTxSize?: number) => { ) as UTxO; }; + let findSellers = async (): Promise => { + return await emulator.getUtxosWithUnit( + builder.sellerAddress, + builder.sellerToken, + ); + }; + let defaultManagerInput: UTxO = { txHash: "00".repeat(32), outputIndex: outputIndex++, @@ -186,7 +194,7 @@ export const genWarehouse = async (maxTxSize?: number) => { lovelace: MANAGER_MIN_ADA, }, address: builder.managerAddress, - datum: builder.toDatumManager(defaultManagerDatum), + datum: WarehouseBuilder.toDatumManager(defaultManagerDatum), }; let defaultSellerInput: UTxO = { @@ -197,7 +205,7 @@ export const genWarehouse = async (maxTxSize?: number) => { lovelace: SELLER_MIN_ADA, }, address: builder.sellerAddress, - datum: builder.toDatumSeller(defaultSellerDatum), + datum: WarehouseBuilder.toDatumSeller(defaultSellerDatum), }; let defaultOrderInput: UTxO = { @@ -208,7 +216,7 @@ export const genWarehouse = async (maxTxSize?: number) => { lovelace: ORDER_MIN_ADA, }, address: builder.orderAddress, - datum: builder.toDatumOrder(defaultOrderDatum), + datum: WarehouseBuilder.toDatumOrder(defaultOrderDatum), }; let defaultTreasuryInput: UTxO = { @@ -223,7 +231,7 @@ export const genWarehouse = async (maxTxSize?: number) => { )]: defaultTreasuryDatum.reserveBase, }, address: builder.treasuryAddress, - datum: builder.toDatumTreasury(defaultTreasuryDatum), + datum: WarehouseBuilder.toDatumTreasury(defaultTreasuryDatum), }; return { @@ -240,6 +248,7 @@ export const genWarehouse = async (maxTxSize?: number) => { defaultOrderDatum, ammPoolInput, findTreasuryInput, + findSellers, ammPoolDatum, builder, outputIndex, diff --git a/src/types.ts b/src/types.ts index 14f862e..a67b7ed 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,12 +8,20 @@ import type { SellerValidateSellerSpending, TreasuryValidateTreasurySpending, } from "../plutus"; +import type { FactoryValidatorValidateFactory } from "../amm-plutus"; export type Address = T.Address; export type Assets = T.Assets; export type BluePrintAsset = { policyId: string; assetName: string }; +export type Credential = T.Credential; +export type CTransactionHash = T.CTransactionHash; +export type CTransactionOutput = T.CTransactionOutput; +export type CTransactionOutputs = T.CTransactionOutputs; +export type CTransactionUnspentOutputs = T.CTransactionUnspentOutputs; export type Datum = T.Datum; export type Emulator = T.Emulator; +export type Maestro = T.Maestro; +export type MaestroSupportedNetworks = T.MaestroSupportedNetworks; export type Network = T.Network; export type OutputData = T.OutputData; export type OutRef = T.OutRef; @@ -26,14 +34,13 @@ export type Script = T.Script; export type ScriptRef = T.ScriptRef; export type Translucent = T.Translucent; export type Tx = T.Tx; +export type TxSigned = T.TxSigned; export type Unit = T.Unit; export type UnixTime = T.UnixTime; export type UTxO = T.UTxO; -export type Maestro = T.Maestro; -export type MaestroSupportedNetworks = T.MaestroSupportedNetworks; -export type Credential = T.Credential; export type AmmPoolDatum = FeedTypeAmmPool["_datum"]; +export type AmmFactoryDatum = FactoryValidatorValidateFactory["datum"]; export type FactoryDatum = FactoryValidateFactory["datum"]; export type ManagerDatum = ManagerValidateManagerSpending["managerInDatum"]; export type OrderDatum = OrderValidateOrder["datum"]; diff --git a/src/utils.ts b/src/utils.ts index 4fc99d5..7d92943 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -224,3 +224,15 @@ export function calculateInitialLiquidity( } return x; } + +export function catchWrapper( + func: (t: T) => R, + arg0: T, + defaultValue: R, +): R { + try { + return func(arg0); + } catch { + return defaultValue; + } +}