diff --git a/src/core/errors.ts b/src/core/errors.ts index 9247115..cf7daa2 100644 --- a/src/core/errors.ts +++ b/src/core/errors.ts @@ -1,7 +1,10 @@ -import {Data} from "effect" +import { Data } from "effect"; export class NoUTXOsInScriptError { readonly _tag = "NoUTXOsInScriptError"; } +export class NoUTXOsInWallet { + readonly _tag = "NoUTXOsInScriptError"; +} export class MissingDatumError { readonly _tag = "NoUTXOsInScriptError"; } @@ -10,7 +13,10 @@ export class InvalidDatumError { readonly _tag = "InvalidDatumError"; } -export class TransactionError extends Data.TaggedError("TransactionError")<{ - message: string +export class FromAddressError extends Data.TaggedError("FromAddressError")<{ + message: string; }> {} +export class TransactionError extends Data.TaggedError("TransactionError")<{ + message: string; +}> {} diff --git a/src/endpoints/Contract.ts b/src/endpoints/Contract.ts index 06a42ae..c946783 100644 --- a/src/endpoints/Contract.ts +++ b/src/endpoints/Contract.ts @@ -1,9 +1,6 @@ import { ContractConfig, - LockTokensConfig, Lucid, - Result, - TxComplete, VestingDatum, collectVestingTokens, getVestingByAddress, @@ -12,6 +9,7 @@ import { } from "../index.js"; import linearVesting from "../../test/linearVesting.json" assert { type: "json" }; +// eslint-disable-next-line @typescript-eslint/no-unused-vars export const withMesh = (_scripts: ContractConfig) => (mesh: string) => { return { lockTokens: () => { diff --git a/src/endpoints/collectVestingTokens.ts b/src/endpoints/collectVestingTokens.ts index fcc3ce3..ff86931 100644 --- a/src/endpoints/collectVestingTokens.ts +++ b/src/endpoints/collectVestingTokens.ts @@ -8,7 +8,7 @@ import { divCeil, safeParseDatum, toAddress } from "../core/utils/utils.js"; import { CollectPartialConfig, CollectPartialScripts } from "../core/types.js"; import { VestingRedeemer, VestingDatum } from "../core/contract.types.js"; import { TIME_TOLERANCE_MS } from "../index.js"; -import { Effect, Either } from "effect"; +import { Effect } from "effect"; import { TransactionError } from "../core/errors.js"; export const collectVestingTokens = diff --git a/src/endpoints/lockVestingTokens.ts b/src/endpoints/lockVestingTokens.ts index 2169760..669a39b 100644 --- a/src/endpoints/lockVestingTokens.ts +++ b/src/endpoints/lockVestingTokens.ts @@ -2,90 +2,103 @@ import { Lucid, SpendingValidator, Data, - TxComplete, toUnit, } from "@anastasia-labs/lucid-cardano-fork"; import { fromAddress } from "../core/utils/utils.js"; -import { LockTokensConfig, LockTokensScripts, Result } from "../core/types.js"; +import { LockTokensConfig, LockTokensScripts } from "../core/types.js"; import { VestingDatum } from "../core/contract.types.js"; import { PROTOCOL_FEE, PROTOCOL_PAYMENT_KEY, PROTOCOL_STAKE_KEY, } from "../index.js"; +import { Effect, pipe } from "effect"; +import { + FromAddressError, + NoUTXOsInWallet, + TransactionError, +} from "../core/errors.js"; export const lockTokens = (scripts: LockTokensScripts) => (lucid: Lucid) => (config: LockTokensConfig) => { - return { - build: async (): Promise> => { - const walletUtxos = await lucid.wallet.getUtxos(); - if (!walletUtxos.length) - return { type: "error", error: new Error("No utxos in wallet") }; + const program = Effect.gen(function* ($) { + const walletUtxos = yield* $( + Effect.promise(() => lucid.wallet.getUtxos()) + ); + if (!walletUtxos.length) yield* $(Effect.fail(new NoUTXOsInWallet())); - const vestingValidator: SpendingValidator = { - type: "PlutusV2", - script: scripts.vesting, - }; - const validatorAddress = - lucid.utils.validatorToAddress(vestingValidator); + const vestingValidator: SpendingValidator = { + type: "PlutusV2", + script: scripts.vesting, + }; + const validatorAddress = lucid.utils.validatorToAddress(vestingValidator); - const datum = Data.to( - { - beneficiary: fromAddress(config.beneficiary), - assetClass: { - symbol: config.vestingAsset.policyId, - name: config.vestingAsset.tokenName, - }, - totalVestingQty: BigInt( - config.totalVestingQty - config.totalVestingQty * PROTOCOL_FEE - ), - vestingPeriodStart: BigInt(config.vestingPeriodStart), - vestingPeriodEnd: BigInt(config.vestingPeriodEnd), - firstUnlockPossibleAfter: BigInt(config.firstUnlockPossibleAfter), - totalInstallments: BigInt(config.totalInstallments), + const safeBeneficiary = yield* $( + Effect.try({ + try: () => fromAddress(config.beneficiary), + catch: (error) => new FromAddressError({ message: String(error) }), + }) + ); + + const datum = Data.to( + { + beneficiary: safeBeneficiary, + assetClass: { + symbol: config.vestingAsset.policyId, + name: config.vestingAsset.tokenName, }, - VestingDatum - ); + totalVestingQty: BigInt( + config.totalVestingQty - config.totalVestingQty * PROTOCOL_FEE + ), + vestingPeriodStart: BigInt(config.vestingPeriodStart), + vestingPeriodEnd: BigInt(config.vestingPeriodEnd), + firstUnlockPossibleAfter: BigInt(config.firstUnlockPossibleAfter), + totalInstallments: BigInt(config.totalInstallments), + }, + VestingDatum + ); - const unit = config.vestingAsset.policyId - ? toUnit(config.vestingAsset.policyId, config.vestingAsset.tokenName) - : "lovelace"; + const unit = config.vestingAsset.policyId + ? toUnit(config.vestingAsset.policyId, config.vestingAsset.tokenName) + : "lovelace"; - try { - const tx = await lucid - .newTx() - .collectFrom(walletUtxos) - .payToContract( - validatorAddress, - { inline: datum }, - { - [unit]: BigInt( - config.totalVestingQty - config.totalVestingQty * PROTOCOL_FEE + return yield* $( + Effect.tryPromise({ + try: () => { + const tx = lucid + .newTx() + .collectFrom(walletUtxos) + .payToContract( + validatorAddress, + { inline: datum }, + { + [unit]: BigInt( + config.totalVestingQty - + config.totalVestingQty * PROTOCOL_FEE + ), + } + ) + .payToAddress( + lucid.utils.credentialToAddress( + lucid.utils.keyHashToCredential(PROTOCOL_PAYMENT_KEY), + lucid.utils.keyHashToCredential(PROTOCOL_STAKE_KEY) ), - } - ) - .payToAddress( - lucid.utils.credentialToAddress( - lucid.utils.keyHashToCredential(PROTOCOL_PAYMENT_KEY), - lucid.utils.keyHashToCredential(PROTOCOL_STAKE_KEY) - ), - { - [unit]: BigInt(config.totalVestingQty * PROTOCOL_FEE), - } - ) - .complete(); - - return { type: "ok", data: tx }; - } catch (error) { - if (error instanceof Error) return { type: "error", error: error }; - - return { - type: "error", - error: new Error(`${JSON.stringify(error)}`), - }; - } - }, + { + [unit]: BigInt(config.totalVestingQty * PROTOCOL_FEE), + } + ) + .complete(); + return tx; + }, + catch: (e) => new TransactionError({ message: String(e) }), + }) + ); + }); + return { + program: () => program, + build: () => pipe(program, Effect.either, Effect.runPromise), + unsafeBuild: () => pipe(program, Effect.runPromise), }; }; diff --git a/test/helpers.ts b/test/helpers.ts new file mode 100644 index 0000000..5e2bc7f --- /dev/null +++ b/test/helpers.ts @@ -0,0 +1,25 @@ +import { expect } from "vitest"; +import { Emulator, Result, TxComplete } from "../src"; +import { Contract } from "../src/endpoints/Contract"; +import { pipe } from "effect"; + +export const submitAction = async (tx: Result) => { + expect(tx.type).toBe("ok"); + if (tx.type === "ok") { + return await (await tx.data.sign().complete()).submit(); + } +}; + +export const collectAction = async ( + user: Awaited>, + emulator: Emulator +) => + pipe( + await user + .collectVestingTokens({ + vestingOutRef: (await user.getVestedTokens())[0].outRef, + currentTime: emulator.now(), + }) + .build(), + submitAction + ); diff --git a/test/lock-unlock.test.ts b/test/lock-unlock.test.ts index 117d513..48b5db3 100644 --- a/test/lock-unlock.test.ts +++ b/test/lock-unlock.test.ts @@ -60,8 +60,8 @@ test("Test - LockTokens, Unlock Tokens", async ({ users, emulator, }) => { - await pipe( - await users[0] + const tx0 = await pipe( + users[0] .lockTokens({ beneficiary: await users[1].lucid.wallet.address(), vestingAsset: { @@ -74,9 +74,13 @@ test("Test - LockTokens, Unlock Tokens", async ({ firstUnlockPossibleAfter: emulator.now(), totalInstallments: 4, }) - .build(), - submitAction + .program(), + Effect.andThen((tx) => Effect.promise(() => tx.sign().complete())), + Effect.andThen((tx) => Effect.promise(() => tx.submit())), + Effect.either, + Effect.runPromise ); + expect(Either.isRight(tx0)).toBe(true); // //NOTE: INSTALLMENT 1 emulator.awaitBlock(1080);