From 3b95345afb1d9c3955a3e92dea316e7cd3cf862c Mon Sep 17 00:00:00 2001 From: Calvin Koepke Date: Mon, 16 Oct 2023 16:54:06 -0600 Subject: [PATCH 1/5] wip: update and withdraw tx --- .../demo/src/components/Actions/Actions.tsx | 14 +- .../Actions/modules/DepositTasteTest.tsx | 6 +- .../Actions/modules/UpdateTasteTest.tsx | 91 ++++ .../Actions/modules/WithdrawTasteTest.tsx | 93 ++++ packages/taste-test/src/@types/index.ts | 20 +- packages/taste-test/src/TasteTest.class.ts | 234 --------- packages/taste-test/src/index.ts | 2 +- .../lib/classes/AbstractTasteTest.class.ts | 4 +- .../src/lib/classes/TasteTest.class.ts | 476 ++++++++++++++++++ packages/taste-test/src/utils.ts | 59 +++ 10 files changed, 748 insertions(+), 251 deletions(-) create mode 100644 packages/demo/src/components/Actions/modules/UpdateTasteTest.tsx create mode 100644 packages/demo/src/components/Actions/modules/WithdrawTasteTest.tsx delete mode 100644 packages/taste-test/src/TasteTest.class.ts create mode 100644 packages/taste-test/src/lib/classes/TasteTest.class.ts diff --git a/packages/demo/src/components/Actions/Actions.tsx b/packages/demo/src/components/Actions/Actions.tsx index e97544f7..687f527f 100644 --- a/packages/demo/src/components/Actions/Actions.tsx +++ b/packages/demo/src/components/Actions/Actions.tsx @@ -9,7 +9,9 @@ import { SwapAB } from "./modules/SwapAB"; import { SwapBA } from "./modules/SwapBA"; import { Unlock } from "./modules/UnlockAssets"; import { UpdateSwap } from "./modules/UpdateSwap"; +import { UpdateTasteTest } from "./modules/UpdateTasteTest"; import { Withdraw } from "./modules/Withdraw"; +import { WithdrawTasteTest } from "./modules/WithdrawTasteTest"; import { Zap } from "./modules/Zap"; export const poolQuery: IPoolQuery = { @@ -78,9 +80,17 @@ export const Actions: FC = () => { -
-

Taste Test

+
+

Taste Tests

+
+
+ + {cbor.hash && ( <> diff --git a/packages/demo/src/components/Actions/modules/DepositTasteTest.tsx b/packages/demo/src/components/Actions/modules/DepositTasteTest.tsx index 2f8ef436..dd1111ee 100644 --- a/packages/demo/src/components/Actions/modules/DepositTasteTest.tsx +++ b/packages/demo/src/components/Actions/modules/DepositTasteTest.tsx @@ -1,11 +1,11 @@ import { AssetAmount } from "@sundaeswap/asset"; import { FC, useCallback, useState } from "react"; -import { useAppState } from "../../../state/context"; -import { ActionArgs } from "../Actions"; -import Button from "../../Button"; import { TasteTest } from "@sundaeswap/taste-test"; import { Lucid } from "lucid-cardano"; +import { useAppState } from "../../../state/context"; +import Button from "../../Button"; +import { ActionArgs } from "../Actions"; export const DepositTasteTest: FC = ({ setCBOR, diff --git a/packages/demo/src/components/Actions/modules/UpdateTasteTest.tsx b/packages/demo/src/components/Actions/modules/UpdateTasteTest.tsx new file mode 100644 index 00000000..94ac0f86 --- /dev/null +++ b/packages/demo/src/components/Actions/modules/UpdateTasteTest.tsx @@ -0,0 +1,91 @@ +import { AssetAmount } from "@sundaeswap/asset"; +import { FC, useCallback, useState } from "react"; + +import { TasteTest } from "@sundaeswap/taste-test"; +import { Lucid } from "lucid-cardano"; +import { useAppState } from "../../../state/context"; +import Button from "../../Button"; +import { ActionArgs } from "../Actions"; + +export const UpdateTasteTest: FC = ({ + setCBOR, + setFees, + submit, +}) => { + const { SDK, ready, walletAddress, useReferral } = useAppState(); + const [updating, setUpdating] = useState(false); + + const handleDeposit = useCallback(async () => { + if (!SDK) { + return; + } + + const builderWallet = SDK.build().wallet; + if (!builderWallet) { + return; + } + + const tt = new TasteTest(builderWallet); + setUpdating(true); + try { + await tt + .update({ + assetAmount: new AssetAmount(5000000n, 6), + scripts: { + policy: { + txHash: + "ba464546272ac37694ba86d3e2021a63189704259a708d83ded54b6eba9b721d", + outputIndex: 0, + }, + validator: { + txHash: + "5ce83772dabedc1eeea992b68eb232c42dbd59ba260e057c890bfa77364bc7f3", + outputIndex: 0, + }, + }, + ...(useReferral + ? { + referralFee: { + destination: + "addr_test1qp6crwxyfwah6hy7v9yu5w6z2w4zcu53qxakk8ynld8fgcpxjae5d7xztgf0vyq7pgrrsk466xxk25cdggpq82zkpdcsdkpc68", + payment: new AssetAmount(1000000n, { + assetId: "", + decimals: 6, + }), + }, + } + : {}), + }) + .then(async ({ build, fees }) => { + setFees(fees); + const builtTx = await build(); + + if (submit) { + const { cbor, submit } = await builtTx.sign(); + setCBOR({ + cbor, + hash: await submit(), + }); + } else { + setCBOR({ + cbor: builtTx.cbor, + }); + } + }); + } catch (e) { + console.log(e); + } + + setUpdating(false); + }, [SDK, submit, walletAddress, useReferral]); + + if (!SDK) { + return null; + } + + return ( + + ); +}; diff --git a/packages/demo/src/components/Actions/modules/WithdrawTasteTest.tsx b/packages/demo/src/components/Actions/modules/WithdrawTasteTest.tsx new file mode 100644 index 00000000..d09ca7ec --- /dev/null +++ b/packages/demo/src/components/Actions/modules/WithdrawTasteTest.tsx @@ -0,0 +1,93 @@ +import { AssetAmount } from "@sundaeswap/asset"; +import { FC, useCallback, useState } from "react"; + +import { TasteTest } from "@sundaeswap/taste-test"; +import { Lucid } from "lucid-cardano"; +import { useAppState } from "../../../state/context"; +import Button from "../../Button"; +import { ActionArgs } from "../Actions"; + +export const WithdrawTasteTest: FC = ({ + setCBOR, + setFees, + submit, +}) => { + const { SDK, ready, walletAddress, useReferral } = useAppState(); + const [withdrawing, setWithdrawing] = useState(false); + + const handleDeposit = useCallback(async () => { + if (!SDK) { + return; + } + + const builderWallet = SDK.build().wallet; + if (!builderWallet) { + return; + } + + const tt = new TasteTest(builderWallet); + setWithdrawing(true); + try { + await tt + .withdraw({ + deadline: new Date().setMonth(11), + penaltyAddress: + "addr_test1qp6crwxyfwah6hy7v9yu5w6z2w4zcu53qxakk8ynld8fgcpxjae5d7xztgf0vyq7pgrrsk466xxk25cdggpq82zkpdcsdkpc68", + scripts: { + policy: { + txHash: + "ba464546272ac37694ba86d3e2021a63189704259a708d83ded54b6eba9b721d", + outputIndex: 0, + }, + validator: { + txHash: + "5ce83772dabedc1eeea992b68eb232c42dbd59ba260e057c890bfa77364bc7f3", + outputIndex: 0, + }, + }, + ...(useReferral + ? { + referralFee: { + destination: + "addr_test1qp6crwxyfwah6hy7v9yu5w6z2w4zcu53qxakk8ynld8fgcpxjae5d7xztgf0vyq7pgrrsk466xxk25cdggpq82zkpdcsdkpc68", + payment: new AssetAmount(1000000n, { + assetId: "", + decimals: 6, + }), + }, + } + : {}), + }) + .then(async ({ build, fees }) => { + setFees(fees); + const builtTx = await build(); + + if (submit) { + const { cbor, submit } = await builtTx.sign(); + setCBOR({ + cbor, + hash: await submit(), + }); + } else { + setCBOR({ + cbor: builtTx.cbor, + }); + } + }); + } catch (e) { + console.log(e); + } + + setWithdrawing(false); + }, [SDK, submit, walletAddress, useReferral]); + + if (!SDK) { + return null; + } + + return ( + + ); +}; diff --git a/packages/taste-test/src/@types/index.ts b/packages/taste-test/src/@types/index.ts index b753183a..df335ff6 100644 --- a/packages/taste-test/src/@types/index.ts +++ b/packages/taste-test/src/@types/index.ts @@ -7,8 +7,7 @@ import type { UTxO, } from "lucid-cardano"; -export interface IDepositArgs { - assetAmount: AssetAmount; +export interface IBaseArgs { currentTime?: number; referralFee?: ITxBuilderReferralFee; scripts: { @@ -18,14 +17,15 @@ export interface IDepositArgs { utxos?: UTxO[]; } -export interface IWithdrawArgs { - currentTime?: number; +export interface IDepositArgs extends IBaseArgs { + assetAmount: AssetAmount; +} + +export interface IUpdateArgs extends IDepositArgs { + assetAmount: AssetAmount; +} + +export interface IWithdrawArgs extends IBaseArgs { deadline: number; penaltyAddress: string; - referralFee?: ITxBuilderReferralFee; - scripts: { - policy: MintingPolicy | OutRef; - validator: SpendingValidator | OutRef; - }; - utxos?: UTxO[]; } diff --git a/packages/taste-test/src/TasteTest.class.ts b/packages/taste-test/src/TasteTest.class.ts deleted file mode 100644 index 186155c1..00000000 --- a/packages/taste-test/src/TasteTest.class.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { AssetAmount } from "@sundaeswap/asset"; -import type { - ITxBuilder, - ITxBuilderFees, - ITxBuilderReferralFee, -} from "@sundaeswap/core"; -import { - Data, - Datum, - SpendingValidator, - Tx, - TxComplete, - UTxO, - fromText, - toUnit, - type Lucid, - type MintingPolicy, -} from "lucid-cardano"; - -import { IDepositArgs } from "./@types"; -import { - DiscoveryNodeAction, - NodeValidatorAction, - SetNode, -} from "./@types/contracts"; -import { AbstractTasteTest } from "./lib/classes/AbstractTasteTest.class"; -import { - FOLDING_FEE_ADA, - MIN_COMMITMENT_ADA, - NODE_ADA, - SETNODE_PREFIX, - TIME_TOLERANCE_MS, -} from "./lib/contants"; -import { findCoveringNode } from "./utils"; - -/** - * Object arguments for completing a transaction. - */ -interface ITasteTestCompleteTxArgs { - tx: Tx; - referralFee?: ITxBuilderReferralFee; - datum?: string; - complete?: boolean; -} - -export class TasteTest implements AbstractTasteTest { - constructor(public lucid: Lucid) {} - - public async deposit( - args: IDepositArgs - ): Promise> { - const walletUtxos = await this.lucid.wallet.getUtxos(); - const walletAddress = await this.lucid.wallet.address(); - - if (!walletUtxos.length) { - throw new Error("No available UTXOs found in wallet."); - } - - if (!args.scripts) { - throw new Error( - "Did not receive a reference script UTXO or raw PlutusV2 CBOR script for the validator." - ); - } - - let nodeValidator: SpendingValidator; - if ("txHash" in args.scripts.validator) { - const script = ( - await this.lucid.provider.getUtxosByOutRef([args.scripts.validator]) - )?.[0]; - if (!script?.scriptRef) { - throw new Error( - "Could not derive UTXO from supplied OutRef in scripts.validator." - ); - } - nodeValidator = script.scriptRef; - } else { - nodeValidator = args.scripts.validator; - } - - const nodeValidatorAddr = - this.lucid.utils.validatorToAddress(nodeValidator); - - let nodePolicy: MintingPolicy; - if ("txHash" in args.scripts.policy) { - const script = ( - await this.lucid.provider.getUtxosByOutRef([args.scripts.policy]) - )?.[0]; - if (!script?.scriptRef) { - throw new Error( - "Could not derive UTXO from supplied OutRef in scripts.policy." - ); - } - nodePolicy = script.scriptRef; - } else { - nodePolicy = args.scripts.policy; - } - - const nodePolicyId = this.lucid.utils.mintingPolicyToId(nodePolicy); - - const userKey = - this.lucid.utils.getAddressDetails(walletAddress).paymentCredential?.hash; - - if (!userKey) { - throw new Error("Missing wallet's payment credential hash."); - } - - let coveringNode: UTxO | undefined; - if (args.utxos) { - coveringNode = args.utxos[0]; - } else { - const nodeUTXOs = await this.lucid.utxosAt(nodeValidatorAddr); - coveringNode = findCoveringNode(nodeUTXOs, userKey); - } - - if (!coveringNode || !coveringNode.datum) { - throw new Error("Could not find covering node."); - } - - const coveringNodeDatum = Data.from(coveringNode.datum, SetNode); - - const prevNodeDatum = Data.to( - { - key: coveringNodeDatum.key, - next: userKey, - }, - SetNode - ); - - const nodeDatum = Data.to( - { - key: userKey, - next: coveringNodeDatum.next, - }, - SetNode - ); - - const redeemerNodePolicy = Data.to( - { - PInsert: { - keyToInsert: userKey, - coveringNode: coveringNodeDatum, - }, - }, - DiscoveryNodeAction - ); - - const redeemerNodeValidator = Data.to("LinkedListAct", NodeValidatorAction); - - const assets = { - [toUnit(nodePolicyId, `${fromText(SETNODE_PREFIX)}${userKey}`)]: 1n, - }; - - const correctAmount = BigInt(args.assetAmount.amount) + MIN_COMMITMENT_ADA; - - const upperBound = (args?.currentTime ?? Date.now()) + TIME_TOLERANCE_MS; - const lowerBound = (args?.currentTime ?? Date.now()) - TIME_TOLERANCE_MS; - - const tx = this.lucid - .newTx() - .collectFrom([coveringNode], redeemerNodeValidator) - .compose(this.lucid.newTx().attachSpendingValidator(nodeValidator)) - .payToContract( - nodeValidatorAddr, - { inline: prevNodeDatum }, - coveringNode.assets - ) - .payToContract( - nodeValidatorAddr, - { inline: nodeDatum }, - { ...assets, lovelace: correctAmount } - ) - .addSignerKey(userKey) - .mintAssets(assets, redeemerNodePolicy) - .compose(this.lucid.newTx().attachMintingPolicy(nodePolicy)) - .validFrom(lowerBound) - .validTo(upperBound); - - return this.completeTx({ tx, referralFee: args.referralFee }); - } - - private async completeTx({ - tx, - referralFee, - }: ITasteTestCompleteTxArgs): Promise> { - if (referralFee) { - if (referralFee.payment.metadata.assetId !== "") { - tx.payToAddress(referralFee.destination, { - [referralFee.payment.metadata.assetId]: referralFee.payment.amount, - }); - } else { - tx.payToAddress(referralFee.destination, { - lovelace: referralFee.payment.amount, - }); - } - } - - const baseFees: Omit = { - deposit: new AssetAmount(NODE_ADA, 6), - referral: referralFee?.payment, - foldFee: new AssetAmount(FOLDING_FEE_ADA, 6), - scooperFee: new AssetAmount(0n), - }; - - const txFee = tx.txBuilder.get_fee_if_set(); - let finishedTx: TxComplete | undefined; - const thisTx: ITxBuilder = { - tx, - fees: baseFees, - datum: Buffer.from(tx.txBuilder.to_bytes()).toString("hex"), - async build() { - if (!finishedTx) { - finishedTx = await tx.complete(); - thisTx.fees.cardanoTxFee = new AssetAmount( - BigInt(txFee?.to_str() ?? finishedTx?.fee?.toString() ?? "0"), - 6 - ); - } - - return { - cbor: Buffer.from(finishedTx.txComplete.to_bytes()).toString("hex"), - sign: async () => { - const signedTx = await (finishedTx as TxComplete).sign().complete(); - return { - cbor: Buffer.from(signedTx.txSigned.to_bytes()).toString("hex"), - submit: async () => await signedTx.submit(), - }; - }, - }; - }, - }; - - return thisTx; - } -} diff --git a/packages/taste-test/src/index.ts b/packages/taste-test/src/index.ts index 197dab64..beb6ee69 100644 --- a/packages/taste-test/src/index.ts +++ b/packages/taste-test/src/index.ts @@ -57,4 +57,4 @@ * @module TasteTest * @packageDocumentation */ -export { TasteTest } from "./TasteTest.class"; +export { TasteTest } from "./lib/classes/TasteTest.class"; diff --git a/packages/taste-test/src/lib/classes/AbstractTasteTest.class.ts b/packages/taste-test/src/lib/classes/AbstractTasteTest.class.ts index f3cfb764..fa596007 100644 --- a/packages/taste-test/src/lib/classes/AbstractTasteTest.class.ts +++ b/packages/taste-test/src/lib/classes/AbstractTasteTest.class.ts @@ -1,10 +1,12 @@ import type { ITxBuilder } from "@sundaeswap/core"; import type { Lucid } from "lucid-cardano"; -import { IDepositArgs } from "../../@types"; +import { IDepositArgs, IUpdateArgs, IWithdrawArgs } from "../../@types"; export abstract class AbstractTasteTest { abstract lucid: Lucid; abstract deposit(args: IDepositArgs): Promise; + abstract update(args: IUpdateArgs): Promise; + abstract withdraw(args: IWithdrawArgs): Promise; } diff --git a/packages/taste-test/src/lib/classes/TasteTest.class.ts b/packages/taste-test/src/lib/classes/TasteTest.class.ts new file mode 100644 index 00000000..518d08e7 --- /dev/null +++ b/packages/taste-test/src/lib/classes/TasteTest.class.ts @@ -0,0 +1,476 @@ +import { AssetAmount } from "@sundaeswap/asset"; +import type { + ITxBuilder, + ITxBuilderFees, + ITxBuilderReferralFee, +} from "@sundaeswap/core"; +import { + Assets, + Data, + Datum, + SpendingValidator, + Tx, + TxComplete, + UTxO, + fromText, + toUnit, + type Lucid, + type MintingPolicy, +} from "lucid-cardano"; + +import { + IBaseArgs, + IDepositArgs, + IUpdateArgs, + IWithdrawArgs, +} from "../../@types"; +import { + DiscoveryNodeAction, + NodeValidatorAction, + SetNode, +} from "../../@types/contracts"; +import { + divCeil, + findCoveringNode, + findOwnNode, + findPrevNode, +} from "../../utils"; +import { + FOLDING_FEE_ADA, + MIN_COMMITMENT_ADA, + NODE_ADA, + SETNODE_PREFIX, + TIME_TOLERANCE_MS, + TWENTY_FOUR_HOURS_MS, +} from "../contants"; +import { AbstractTasteTest } from "./AbstractTasteTest.class"; + +/** + * Object arguments for completing a transaction. + */ +interface ITasteTestCompleteTxArgs { + tx: Tx; + referralFee?: ITxBuilderReferralFee; + datum?: string; + complete?: boolean; +} + +export class TasteTest implements AbstractTasteTest { + constructor(public lucid: Lucid) {} + + public async deposit( + args: IDepositArgs + ): Promise> { + const walletUtxos = await this.lucid.wallet.getUtxos(); + const walletAddress = await this.lucid.wallet.address(); + + if (!walletUtxos.length) { + throw new Error("No available UTXOs found in wallet."); + } + + if (!args.scripts) { + throw new Error( + "Did not receive a reference script UTXO or raw PlutusV2 CBOR script for the validator." + ); + } + + const nodeValidator = await this.getNodeValidatorFromArgs(args); + const nodeValidatorAddr = + this.lucid.utils.validatorToAddress(nodeValidator); + const nodePolicy = await this.getNodePolicyFromArgs(args); + const nodePolicyId = this.lucid.utils.mintingPolicyToId(nodePolicy); + + const userKey = + this.lucid.utils.getAddressDetails(walletAddress).paymentCredential?.hash; + + if (!userKey) { + throw new Error("Missing wallet's payment credential hash."); + } + + let coveringNode: UTxO | undefined; + if (args.utxos) { + coveringNode = args.utxos[0]; + } else { + const nodeUTXOs = await this.lucid.utxosAt(nodeValidatorAddr); + coveringNode = findCoveringNode(nodeUTXOs, userKey); + } + + if (!coveringNode || !coveringNode.datum) { + throw new Error("Could not find covering node."); + } + + const coveringNodeDatum = Data.from(coveringNode.datum, SetNode); + + const prevNodeDatum = Data.to( + { + key: coveringNodeDatum.key, + next: userKey, + }, + SetNode + ); + + const nodeDatum = Data.to( + { + key: userKey, + next: coveringNodeDatum.next, + }, + SetNode + ); + + const redeemerNodePolicy = Data.to( + { + PInsert: { + keyToInsert: userKey, + coveringNode: coveringNodeDatum, + }, + }, + DiscoveryNodeAction + ); + + const redeemerNodeValidator = Data.to("LinkedListAct", NodeValidatorAction); + + const assets = { + [toUnit(nodePolicyId, `${fromText(SETNODE_PREFIX)}${userKey}`)]: 1n, + }; + + const correctAmount = BigInt(args.assetAmount.amount) + MIN_COMMITMENT_ADA; + + const upperBound = (args?.currentTime ?? Date.now()) + TIME_TOLERANCE_MS; + const lowerBound = (args?.currentTime ?? Date.now()) - TIME_TOLERANCE_MS; + + const tx = this.lucid + .newTx() + .collectFrom([coveringNode], redeemerNodeValidator) + .compose(this.lucid.newTx().attachSpendingValidator(nodeValidator)) + .payToContract( + nodeValidatorAddr, + { inline: prevNodeDatum }, + coveringNode.assets + ) + .payToContract( + nodeValidatorAddr, + { inline: nodeDatum }, + { ...assets, lovelace: correctAmount } + ) + .addSignerKey(userKey) + .mintAssets(assets, redeemerNodePolicy) + .compose(this.lucid.newTx().attachMintingPolicy(nodePolicy)) + .validFrom(lowerBound) + .validTo(upperBound); + + return this.completeTx({ tx, referralFee: args.referralFee }); + } + + public async update( + args: IUpdateArgs + ): Promise> { + const nodeValidator = await this.getNodeValidatorFromArgs(args); + const nodeValidatorAddr = + this.lucid.utils.validatorToAddress(nodeValidator); + + const userKey = this.lucid.utils.getAddressDetails( + await this.lucid.wallet.address() + ).paymentCredential?.hash; + + if (!userKey) { + throw new Error("Missing wallet's payment credential hash."); + } + + let ownNode: UTxO | undefined; + if (args.utxos) { + ownNode = args.utxos[0]; + } else { + const nodeUTXOs = await this.lucid.utxosAt(nodeValidatorAddr); + ownNode = findOwnNode(nodeUTXOs, userKey); + } + + if (!ownNode || !ownNode.datum) { + throw new Error("Could not find covering node."); + } + + const redeemerNodeValidator = Data.to( + "ModifyCommitment", + NodeValidatorAction + ); + + const newNodeAssets: Assets = {}; + Object.entries(ownNode.assets).forEach(([key, value]) => { + newNodeAssets[key] = BigInt(value); + }); + + newNodeAssets["lovelace"] = + newNodeAssets["lovelace"] + args.assetAmount.amount; + + const tx = this.lucid + .newTx() + .collectFrom([ownNode], redeemerNodeValidator) + .compose(this.lucid.newTx().attachSpendingValidator(nodeValidator)) + .payToContract( + nodeValidatorAddr, + { inline: ownNode.datum }, + newNodeAssets + ); + + return this.completeTx({ tx, referralFee: args.referralFee }); + } + + public async withdraw( + args: IWithdrawArgs + ): Promise> { + const nodeValidator = await this.getNodeValidatorFromArgs(args); + const nodeValidatorAddr = + this.lucid.utils.validatorToAddress(nodeValidator); + + const nodePolicy = await this.getNodePolicyFromArgs(args); + const nodePolicyId = this.lucid.utils.mintingPolicyToId(nodePolicy); + + const userKey = this.lucid.utils.getAddressDetails( + await this.lucid.wallet.address() + ).paymentCredential?.hash; + + if (!userKey) { + throw new Error("Missing wallet's payment credential hash."); + } + + const nodeUTXOS = args.utxos + ? args.utxos + : await this.lucid.utxosAt(nodeValidatorAddr); + + let ownNode: UTxO | undefined; + if (args.utxos) { + ownNode = nodeUTXOS[0]; + } else { + const nodeUTXOs = await this.lucid.utxosAt(nodeValidatorAddr); + ownNode = findOwnNode(nodeUTXOs, userKey); + } + + if (!ownNode || !ownNode.datum) { + throw new Error("Could not find covering node."); + } + + const nodeDatum = Data.from(ownNode.datum, SetNode); + + let prevNode: UTxO | undefined; + if (args.utxos) { + prevNode = args.utxos[0]; + } else { + prevNode = findPrevNode(nodeUTXOS, userKey); + } + + if (!prevNode || !prevNode.datum) { + throw new Error("Could not find previous node."); + } + + const prevNodeDatum = Data.from(prevNode.datum, SetNode); + + const assets = { + [toUnit(nodePolicyId, fromText(SETNODE_PREFIX) + userKey)]: -1n, + }; + + const newPrevNode: SetNode = { + key: prevNodeDatum.key, + next: nodeDatum.next, + }; + + const newPrevNodeDatum = Data.to(newPrevNode, SetNode); + + const redeemerNodePolicy = Data.to( + { + PRemove: { + keyToRemove: userKey, + coveringNode: newPrevNode, + }, + }, + DiscoveryNodeAction + ); + + const redeemerNodeValidator = Data.to("LinkedListAct", NodeValidatorAction); + const upperBound = (args?.currentTime ?? Date.now()) + TIME_TOLERANCE_MS; + const lowerBound = (args?.currentTime ?? Date.now()) - TIME_TOLERANCE_MS; + + const beforeDeadline = upperBound < args.deadline; + const beforeTwentyFourHours = + upperBound < args.deadline - TWENTY_FOUR_HOURS_MS; + + if (beforeDeadline && beforeTwentyFourHours) { + const tx = this.lucid + .newTx() + .collectFrom([ownNode, prevNode], redeemerNodeValidator) + .compose(this.lucid.newTx().attachSpendingValidator(nodeValidator)) + .payToContract( + nodeValidatorAddr, + { inline: newPrevNodeDatum }, + prevNode.assets + ) + .addSignerKey(userKey) + .mintAssets(assets, redeemerNodePolicy) + // .attachMintingPolicy(nodePolicy) + .compose(this.lucid.newTx().attachMintingPolicy(nodePolicy)) + .validFrom(lowerBound) + .validTo(upperBound); + + return this.completeTx({ tx, referralFee: args.referralFee }); + } else if (beforeDeadline && !beforeTwentyFourHours) { + const penaltyAmount = divCeil(ownNode.assets["lovelace"], 4n); + + const tx = this.lucid + .newTx() + .collectFrom([ownNode, prevNode], redeemerNodeValidator) + // .attachSpendingValidator(nodeValidator) + .compose(this.lucid.newTx().attachSpendingValidator(nodeValidator)) + .payToContract( + nodeValidatorAddr, + { inline: newPrevNodeDatum }, + prevNode.assets + ) + .payToAddress(args.penaltyAddress, { + lovelace: penaltyAmount, + }) + .addSignerKey(userKey) + .mintAssets(assets, redeemerNodePolicy) + // .attachMintingPolicy(nodePolicy) + .compose(this.lucid.newTx().attachMintingPolicy(nodePolicy)) + .validFrom(lowerBound) + .validTo(upperBound); + + return this.completeTx({ tx, referralFee: args.referralFee }); + } + + const tx = this.lucid + .newTx() + .collectFrom([ownNode, prevNode], redeemerNodeValidator) + .compose(this.lucid.newTx().attachSpendingValidator(nodeValidator)) + .payToContract( + nodeValidatorAddr, + { inline: newPrevNodeDatum }, + prevNode.assets + ) + .addSignerKey(userKey) + .mintAssets(assets, redeemerNodePolicy) + .compose(this.lucid.newTx().attachMintingPolicy(nodePolicy)) + .validFrom(lowerBound) + .validTo(upperBound); + + return this.completeTx({ tx, referralFee: args.referralFee }); + } + + private async completeTx({ + tx, + referralFee, + }: ITasteTestCompleteTxArgs): Promise> { + if (referralFee) { + if (referralFee.payment.metadata.assetId !== "") { + tx.payToAddress(referralFee.destination, { + [referralFee.payment.metadata.assetId]: referralFee.payment.amount, + }); + } else { + tx.payToAddress(referralFee.destination, { + lovelace: referralFee.payment.amount, + }); + } + } + + const baseFees: Omit = { + deposit: new AssetAmount(NODE_ADA, 6), + referral: referralFee?.payment, + foldFee: new AssetAmount(FOLDING_FEE_ADA, 6), + scooperFee: new AssetAmount(0n), + }; + + const txFee = tx.txBuilder.get_fee_if_set(); + let finishedTx: TxComplete | undefined; + const thisTx: ITxBuilder = { + tx, + fees: baseFees, + datum: Buffer.from(tx.txBuilder.to_bytes()).toString("hex"), + async build() { + if (!finishedTx) { + finishedTx = await tx.complete(); + thisTx.fees.cardanoTxFee = new AssetAmount( + BigInt(txFee?.to_str() ?? finishedTx?.fee?.toString() ?? "0"), + 6 + ); + } + + return { + cbor: Buffer.from(finishedTx.txComplete.to_bytes()).toString("hex"), + sign: async () => { + const signedTx = await (finishedTx as TxComplete).sign().complete(); + return { + cbor: Buffer.from(signedTx.txSigned.to_bytes()).toString("hex"), + submit: async () => await signedTx.submit(), + }; + }, + }; + }, + }; + + return thisTx; + } + + /** + * Retrieves the node validator based on the provided arguments. + * It either fetches the validator directly from the arguments or derives it from a transaction hash. + * + * @param {IBaseArgs} args - The arguments containing information about scripts. + * @param {Object} args.scripts - Contains the scripts information. + * @param {Object} args.scripts.validator - The validator script or information to derive the script. + * @returns {Promise} - Returns a SpendingValidator. + * @throws Will throw an error if unable to derive UTXO from the provided OutRef in `scripts.validator`. + * @async + * @private + */ + private async getNodeValidatorFromArgs({ + scripts, + }: IBaseArgs): Promise { + let nodeValidator: SpendingValidator; + if ("txHash" in scripts.validator) { + const script = ( + await this.lucid.provider.getUtxosByOutRef([scripts.validator]) + )?.[0]; + if (!script?.scriptRef) { + throw new Error( + "Could not derive UTXO from supplied OutRef in scripts.validator." + ); + } + nodeValidator = script.scriptRef; + } else { + nodeValidator = scripts.validator; + } + + return nodeValidator; + } + + /** + * Retrieves the node minting policy ID based on the provided arguments. + * It either fetches the policy ID directly from the arguments or derives it from a transaction hash. + * + * @param {IBaseArgs} args - The arguments containing information about scripts. + * @param {Object} args.scripts - Contains the scripts information. + * @param {Object} args.scripts.policy - The policy script or information to derive the script. + * @returns {Promise} - Returns a MintingPolicy. + * @throws Will throw an error if unable to derive UTXO from the provided OutRef in `scripts.policy`. + * @async + * @private + */ + private async getNodePolicyFromArgs({ + scripts, + }: IBaseArgs): Promise { + let nodePolicy: MintingPolicy; + if ("txHash" in scripts.policy) { + const script = ( + await this.lucid.provider.getUtxosByOutRef([scripts.policy]) + )?.[0]; + if (!script?.scriptRef) { + throw new Error( + "Could not derive UTXO from supplied OutRef in scripts.policy." + ); + } + nodePolicy = script.scriptRef; + } else { + nodePolicy = scripts.policy; + } + + return nodePolicy; + } +} diff --git a/packages/taste-test/src/utils.ts b/packages/taste-test/src/utils.ts index f6bddee6..c4ed3cd9 100644 --- a/packages/taste-test/src/utils.ts +++ b/packages/taste-test/src/utils.ts @@ -26,3 +26,62 @@ export const findCoveringNode = (utxos: UTxO[], userKey: string) => (datum.next == null || userKey < datum.next) ); }); + +/** + * Searches through a list of UTXOs to find a node owned by the user, identified by a specific key. + * + * @param {UTxO[]} utxos - An array of unspent transaction outputs (UTXOs). + * @param {string} userKey - The unique identifier key for the user, used to find a specific node in the UTXOs. + * @returns {UTxO | undefined} - Returns the UTXO that contains the user's node if found, otherwise returns undefined. + * + * This function iterates through the provided `utxos`, attempting to match the `userKey` with a key within the datum of each UTXO. + * If a match is found, it indicates that the UTXO belongs to the user's node, and this UTXO is returned. + * If no matching node is found in the list of UTXOs, the function returns undefined, indicating the user's node was not found. + */ +export const findOwnNode = (utxos: UTxO[], userKey: string) => + utxos.find((value) => { + if (!value.datum) { + return false; + } + + const nodeData = Data.from(value.datum, SetNode); + return nodeData.key == userKey; + }); + +/** + * Searches through a list of UTXOs to find a node owned by the user, identified by a specific key, and return the next reference (previous to the user's node). + * + * @param {UTxO[]} utxos - An array of unspent transaction outputs (UTXOs). + * @param {string} userKey - The unique identifier key for the user, used to find a specific node in the UTXOs. + * @returns {UTxO | undefined} - Returns the UTXO that contains the previous node of the user's node if found, otherwise returns undefined. + * + * This function iterates through the provided `utxos`, attempting to match the `userKey` with a key within the datum of each UTXO. + * If a match is found, it indicates that the UTXO belongs to the user's node, and the UTXO referenced as the previous node is returned. + * If no matching node is found in the list of UTXOs, the function returns undefined, indicating the previous node of the user's node was not found. + */ +export const findPrevNode = (utxos: UTxO[], userKey: string) => + utxos.find((value) => { + if (!value.datum) { + return false; + } + + const datum = Data.from(value.datum, SetNode); + return datum.next !== null && datum.next == userKey; + }); + +/** + * Performs a division operation on two bigints and rounds up the result. + * + * This function takes two bigint numbers, divides the first by the second, and rounds up the result. + * The rounding up ensures the result is the smallest integer greater than or equal to the actual division result, + * often used when dealing with scenarios where fractional results are not acceptable, and rounding up is required. + * + * Note: This function does not handle cases where 'b' is zero. Zero as the divisor will lead to an error as division by zero is not defined. + * + * @param {bigint} a - The dividend represented as a bigint. + * @param {bigint} b - The divisor represented as a bigint. + * @returns {bigint} - The result of the division, rounded up to the nearest bigint. + */ +export const divCeil = (a: bigint, b: bigint) => { + return 1n + (a - 1n) / b; +}; From bcfdc4e238b4f18f1a83ebf75818d9b3013e2097 Mon Sep 17 00:00:00 2001 From: Calvin Koepke Date: Thu, 19 Oct 2023 13:18:06 -0600 Subject: [PATCH 2/5] chore: clean up --- .../core/enums/Core.EPoolSearchType.md | 25 ++ .../core/interfaces/Core.IPoolData.md | 12 +- .../core/interfaces/Core.IPoolDataAsset.md | 4 +- .../core/interfaces/Core.IPoolQuery.md | 4 +- .../interfaces/Core.IQueryProviderClass.md | 39 ++- docs/typescript/core/modules/Core.md | 1 + .../taste-test/classes/AbstractTasteTest.md | 106 ++++++ .../taste-test/classes/TasteTest.md | 306 ++++++++++++++++++ .../taste-test/interfaces/IBaseArgs.md | 11 + .../taste-test/interfaces/IDepositArgs.md | 11 + .../interfaces/ITasteTestCompleteTxArgs.md | 3 + .../taste-test/interfaces/ITxBuilder.md | 10 + .../taste-test/interfaces/IUpdateArgs.md | 9 + .../taste-test/interfaces/IWithdrawArgs.md | 9 + docs/typescript/taste-test/modules.md | 14 + jest.config.ts | 4 +- packages/core/src/@types/queryprovider.ts | 19 ++ .../QueryProvider.SundaeSwap.ts | 46 +++ .../Actions/modules/DepositTasteTest.tsx | 1 + packages/taste-test/src/@types/index.ts | 13 + packages/taste-test/src/index.ts | 6 +- .../lib/classes/AbstractTasteTest.class.ts | 43 +++ .../src/lib/classes/TasteTest.class.ts | 212 ++++++++++-- packages/taste-test/src/lib/contants.ts | 19 +- packages/taste-test/typedoc.json | 2 +- 25 files changed, 863 insertions(+), 66 deletions(-) create mode 100644 docs/typescript/core/enums/Core.EPoolSearchType.md create mode 100644 docs/typescript/taste-test/classes/AbstractTasteTest.md create mode 100644 docs/typescript/taste-test/classes/TasteTest.md create mode 100644 docs/typescript/taste-test/interfaces/IBaseArgs.md create mode 100644 docs/typescript/taste-test/interfaces/IDepositArgs.md create mode 100644 docs/typescript/taste-test/interfaces/ITasteTestCompleteTxArgs.md create mode 100644 docs/typescript/taste-test/interfaces/ITxBuilder.md create mode 100644 docs/typescript/taste-test/interfaces/IUpdateArgs.md create mode 100644 docs/typescript/taste-test/interfaces/IWithdrawArgs.md diff --git a/docs/typescript/core/enums/Core.EPoolSearchType.md b/docs/typescript/core/enums/Core.EPoolSearchType.md new file mode 100644 index 00000000..87df5b5d --- /dev/null +++ b/docs/typescript/core/enums/Core.EPoolSearchType.md @@ -0,0 +1,25 @@ +# Enumeration: EPoolSearchType + +[Core](../modules/Core.md).EPoolSearchType + +Defines the type of pool list to retrieve. + +## Enumeration Members + +### ALL + +• **ALL** = ``"pools"`` + +#### Defined in + +[@types/queryprovider.ts:11](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L11) + +___ + +### POPULAR + +• **POPULAR** = ``"poolsPopular"`` + +#### Defined in + +[@types/queryprovider.ts:12](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L12) diff --git a/docs/typescript/core/interfaces/Core.IPoolData.md b/docs/typescript/core/interfaces/Core.IPoolData.md index b0bb0a3b..b12a7606 100644 --- a/docs/typescript/core/interfaces/Core.IPoolData.md +++ b/docs/typescript/core/interfaces/Core.IPoolData.md @@ -14,7 +14,7 @@ Asset data for the pool pair, Asset A #### Defined in -[@types/queryprovider.ts:82](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L82) +[@types/queryprovider.ts:101](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L101) ___ @@ -26,7 +26,7 @@ Asset data for the pool pair, Asset B #### Defined in -[@types/queryprovider.ts:84](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L84) +[@types/queryprovider.ts:103](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L103) ___ @@ -38,7 +38,7 @@ The pool fee represented as a string. i.e. 1% === "1" and .03% === "0.03" #### Defined in -[@types/queryprovider.ts:78](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L78) +[@types/queryprovider.ts:97](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L97) ___ @@ -50,7 +50,7 @@ The unique identifier of the pool. Also returned directly via [findPoolIdent](Co #### Defined in -[@types/queryprovider.ts:80](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L80) +[@types/queryprovider.ts:99](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L99) ___ @@ -62,7 +62,7 @@ The pool quantity of [assetA](Core.IPoolData.md#asseta) #### Defined in -[@types/queryprovider.ts:86](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L86) +[@types/queryprovider.ts:105](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L105) ___ @@ -74,4 +74,4 @@ The pool quantity of [assetB](Core.IPoolData.md#assetb) #### Defined in -[@types/queryprovider.ts:88](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L88) +[@types/queryprovider.ts:107](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L107) diff --git a/docs/typescript/core/interfaces/Core.IPoolDataAsset.md b/docs/typescript/core/interfaces/Core.IPoolDataAsset.md index fd166adf..66edc0e9 100644 --- a/docs/typescript/core/interfaces/Core.IPoolDataAsset.md +++ b/docs/typescript/core/interfaces/Core.IPoolDataAsset.md @@ -20,7 +20,7 @@ POLICY_ID_HEX.ASSET_NAME_HEX #### Defined in -[@types/queryprovider.ts:68](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L68) +[@types/queryprovider.ts:87](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L87) ___ @@ -32,4 +32,4 @@ The registered decimal places of the asset. #### Defined in -[@types/queryprovider.ts:70](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L70) +[@types/queryprovider.ts:89](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L89) diff --git a/docs/typescript/core/interfaces/Core.IPoolQuery.md b/docs/typescript/core/interfaces/Core.IPoolQuery.md index 31e90803..f30fc549 100644 --- a/docs/typescript/core/interfaces/Core.IPoolQuery.md +++ b/docs/typescript/core/interfaces/Core.IPoolQuery.md @@ -25,7 +25,7 @@ The desired pool fee as a percentage string. #### Defined in -[@types/queryprovider.ts:55](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L55) +[@types/queryprovider.ts:74](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L74) ___ @@ -37,4 +37,4 @@ The pool pair, as an array of [assetId](Core.IPoolDataAsset.md#assetid) #### Defined in -[@types/queryprovider.ts:53](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L53) +[@types/queryprovider.ts:72](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L72) diff --git a/docs/typescript/core/interfaces/Core.IQueryProviderClass.md b/docs/typescript/core/interfaces/Core.IQueryProviderClass.md index 8757092f..7b09317b 100644 --- a/docs/typescript/core/interfaces/Core.IQueryProviderClass.md +++ b/docs/typescript/core/interfaces/Core.IQueryProviderClass.md @@ -32,7 +32,7 @@ Finds the associated UTXO data of an open order. #### Defined in -[@types/queryprovider.ts:34](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L34) +[@types/queryprovider.ts:53](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L53) ___ @@ -56,9 +56,11 @@ Finds a matching pool on the SundaeSwap protocol. `Promise`<[`IPoolData`](Core.IPoolData.md)\> +Returns the queried pool's data. + #### Defined in -[@types/queryprovider.ts:19](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L19) +[@types/queryprovider.ts:28](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L28) ___ @@ -82,6 +84,37 @@ Finds a matching pool on the SundaeSwap protocol and returns only the ident. `Promise`<`string`\> +Returns queried pool's ident. + +#### Defined in + +[@types/queryprovider.ts:37](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L37) + +___ + +### getAllPools + +• **getAllPools**: (`type?`: [`EPoolSearchType`](../enums/Core.EPoolSearchType.md), `query?`: `string`) => `Promise`<[`IPoolData`](Core.IPoolData.md)[]\> + +#### Type declaration + +▸ (`type?`, `query?`): `Promise`<[`IPoolData`](Core.IPoolData.md)[]\> + +Retrieves all available pools' data. + +##### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `type?` | [`EPoolSearchType`](../enums/Core.EPoolSearchType.md) | The type of search to perform. | +| `query?` | `string` | A string to use as your query. | + +##### Returns + +`Promise`<[`IPoolData`](Core.IPoolData.md)[]\> + +Returns an array of IPoolData objects, each representing the data for an individual pool. + #### Defined in -[@types/queryprovider.ts:27](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L27) +[@types/queryprovider.ts:46](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/core/src/@types/queryprovider.ts#L46) diff --git a/docs/typescript/core/modules/Core.md b/docs/typescript/core/modules/Core.md index 0b4720c2..56401b0b 100644 --- a/docs/typescript/core/modules/Core.md +++ b/docs/typescript/core/modules/Core.md @@ -51,6 +51,7 @@ const txHash = await sdk.swap( /** ... */ ).then(({ submit }) => submit()); ## Enumerations +- [EPoolSearchType](../enums/Core.EPoolSearchType.md) - [PoolCoin](../enums/Core.PoolCoin.md) ## Classes diff --git a/docs/typescript/taste-test/classes/AbstractTasteTest.md b/docs/typescript/taste-test/classes/AbstractTasteTest.md new file mode 100644 index 00000000..c02dd3ba --- /dev/null +++ b/docs/typescript/taste-test/classes/AbstractTasteTest.md @@ -0,0 +1,106 @@ +# Class: AbstractTasteTest + +Represents the abstract class that should be extended to implement +the functionality of the Taste Test features. This class provides +the structure for depositing, updating, and withdrawing operations. + +**`Property`** + +An instance of the Lucid class, representing the core functionality handler. + +## Implemented by + +- [`TasteTest`](TasteTest.md) + +## Properties + +### lucid + +• `Abstract` **lucid**: `Lucid` + +Represents the Lucid instance used for core operations. + +#### Defined in + +[taste-test/src/lib/classes/AbstractTasteTest.class.ts:17](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/taste-test/src/lib/classes/AbstractTasteTest.class.ts#L17) + +## Methods + +### deposit + +▸ `Abstract` **deposit**(`args`): `Promise`<[`ITxBuilder`](../interfaces/ITxBuilder.md)<`unknown`, `unknown`\>\> + +Initiates a deposit transaction. The specific implementation of this method +should handle the business logic associated with making a deposit, including +validations, transaction building, and error handling. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `args` | [`IDepositArgs`](../interfaces/IDepositArgs.md) | The arguments required for the deposit operation, including the amount, user credentials, and other transaction details. | + +#### Returns + +`Promise`<[`ITxBuilder`](../interfaces/ITxBuilder.md)<`unknown`, `unknown`\>\> + +- Returns a promise that resolves with an +ITxBuilder instance, representing the constructed transaction for the deposit. + +#### Defined in + +[taste-test/src/lib/classes/AbstractTasteTest.class.ts:29](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/taste-test/src/lib/classes/AbstractTasteTest.class.ts#L29) + +___ + +### update + +▸ `Abstract` **update**(`args`): `Promise`<[`ITxBuilder`](../interfaces/ITxBuilder.md)<`unknown`, `unknown`\>\> + +Initiates an update transaction. This method is responsible for handling +the business logic necessary to update an existing record or transaction. +This could include changing the amount, modifying references, or other updates. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `args` | [`IUpdateArgs`](../interfaces/IUpdateArgs.md) | The arguments required for the update operation. This includes any fields that are updatable within the transaction and may include credentials for authorization. | + +#### Returns + +`Promise`<[`ITxBuilder`](../interfaces/ITxBuilder.md)<`unknown`, `unknown`\>\> + +- Returns a promise that resolves with an +ITxBuilder instance, representing the constructed transaction for the update. + +#### Defined in + +[taste-test/src/lib/classes/AbstractTasteTest.class.ts:42](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/taste-test/src/lib/classes/AbstractTasteTest.class.ts#L42) + +___ + +### withdraw + +▸ `Abstract` **withdraw**(`args`): `Promise`<[`ITxBuilder`](../interfaces/ITxBuilder.md)<`unknown`, `unknown`\>\> + +Initiates a withdrawal transaction. This method should handle the logic +associated with withdrawing funds, including validations, constructing the +withdrawal transaction, and handling errors appropriately. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `args` | [`IWithdrawArgs`](../interfaces/IWithdrawArgs.md) | The arguments required for the withdrawal operation, including the amount to withdraw, user credentials, and other necessary details. | + +#### Returns + +`Promise`<[`ITxBuilder`](../interfaces/ITxBuilder.md)<`unknown`, `unknown`\>\> + +- Returns a promise that resolves with an +ITxBuilder instance, representing the constructed transaction for the withdrawal. + +#### Defined in + +[taste-test/src/lib/classes/AbstractTasteTest.class.ts:54](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/taste-test/src/lib/classes/AbstractTasteTest.class.ts#L54) diff --git a/docs/typescript/taste-test/classes/TasteTest.md b/docs/typescript/taste-test/classes/TasteTest.md new file mode 100644 index 00000000..40e1e7eb --- /dev/null +++ b/docs/typescript/taste-test/classes/TasteTest.md @@ -0,0 +1,306 @@ +# Class: TasteTest + +Represents the TasteTest class capable of handling various blockchain interactions for a protocol Taste Test. + +This class encapsulates methods for common operations required in the Taste Test process on the blockchain. +It provides an interface to deposit assets, withdraw them, and update the commitment amounts. Each operation +is transaction-based and interacts with blockchain smart contracts to ensure the correct execution of business logic. + +The TasteTest class relies on the Lucid service, which can come either from your own instance or from an existing +SundaeSDK class instance (as shown below). The class methods handle the detailed steps of each operation, including +transaction preparation, fee handling, and error checking, abstracting these complexities from the end user. + +Usage example (not part of the actual documentation): +```ts +import type { Lucid } from "lucid-cardano"; +const tasteTest = new TasteTest(sdk.build().wallet); + +// For depositing assets into the taste test smart contract +tasteTest.deposit({ ... }).then(({ build, fees }) => console.log(fees)); + +// For withdrawing assets from the taste test smart contract +tasteTest.withdraw({ ... }).then(({ build, fees }) => console.log(fees));; + +// For updating the committed assets in the taste test smart contract +tasteTest.update({ ... }).then(({ build, fees }) => console.log(fees));; +``` + +**`Implements`** + +**`Param`** + +An instance of the Lucid class, providing various utility methods for blockchain interactions. + +## Implements + +- [`AbstractTasteTest`](AbstractTasteTest.md) + +## Methods + +### completeTx + +▸ `Private` **completeTx**(`params`): `Promise`<[`ITxBuilder`](../interfaces/ITxBuilder.md)<`Tx`, `undefined` \| `string`\>\> + +Finalizes the construction of a transaction with the necessary operations and fees. + +This function takes a constructed transaction and applies the final necessary steps to make it ready for submission. +These steps include setting the referral fee if it's part of the transaction, calculating the necessary transaction fees, +and preparing the transaction for signing. The function adapts to the presence of asset-specific transactions by +handling different referral payment types. + +The base fees for the transaction are calculated based on a boolean for whether to include them, specifically for deposit and fold. +The only fees that are always set are referral fees and the native Cardano transaction fee. + +Once the transaction is built, it's completed, and the actual Cardano network transaction fee is retrieved and set. +The built transaction is then ready for signing and submission. + +**`Async`** + +**`Throws`** + +Throws an error if the transaction cannot be completed or if there are issues with the fee calculation. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `params` | [`ITasteTestCompleteTxArgs`](../interfaces/ITasteTestCompleteTxArgs.md) | The arguments required for completing the transaction, including the transaction itself, the referral fee, and a flag indicating if the transaction includes fees. | + +#### Returns + +`Promise`<[`ITxBuilder`](../interfaces/ITxBuilder.md)<`Tx`, `undefined` \| `string`\>\> + +- Returns a promise that resolves with a transaction builder object, which includes the transaction, its associated fees, and functions to build, sign, and submit the transaction. + +#### Defined in + +[taste-test/src/lib/classes/TasteTest.class.ts:473](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/taste-test/src/lib/classes/TasteTest.class.ts#L473) + +___ + +### deposit + +▸ **deposit**(`args`): `Promise`<[`ITxBuilder`](../interfaces/ITxBuilder.md)<`Tx`, `undefined` \| `string`\>\> + +Initiates a deposit transaction, conducting various checks and orchestrating the transaction construction. + +This method is in charge of initiating a deposit operation. It first verifies the availability of UTXOs in the wallet +and checks for the presence of necessary scripts. It retrieves the node validator and policy, determines the user's key, +and identifies the covering node based on the provided or found UTXOs. + +If a covering node is not identified, the method checks whether the user already owns a node and, based on the `updateFallback` +flag, either updates the node or throws an error. After identifying the covering node, it prepares the necessary data structures +and transaction components, including the redeemer policy and validator actions. + +The transaction is then constructed, incorporating various elements such as assets, validators, and minting policies. +It ensures the transaction falls within a valid time frame and completes it by setting the appropriate fees and preparing +it for submission. + +**`Async`** + +**`Throws`** + +Throws an error if no UTXOs are available, if reference scripts are missing, or if a covering node cannot be found. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `args` | [`IDepositArgs`](../interfaces/IDepositArgs.md) | The required arguments for the deposit operation. | + +#### Returns + +`Promise`<[`ITxBuilder`](../interfaces/ITxBuilder.md)<`Tx`, `undefined` \| `string`\>\> + +- Returns a promise that resolves with a transaction builder object, +which includes the transaction, its associated fees, and functions to build, sign, and submit the transaction. + +#### Implementation of + +[AbstractTasteTest](AbstractTasteTest.md).[deposit](AbstractTasteTest.md#deposit) + +#### Defined in + +[taste-test/src/lib/classes/TasteTest.class.ts:113](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/taste-test/src/lib/classes/TasteTest.class.ts#L113) + +___ + +### getNodePolicyFromArgs + +▸ `Private` **getNodePolicyFromArgs**(`args`): `Promise`<`Script`\> + +Retrieves the node minting policy ID based on the provided arguments. +It either fetches the policy ID directly from the arguments or derives it from a transaction hash. + +**`Throws`** + +Will throw an error if unable to derive UTXO from the provided OutRef in `scripts.policy`. + +**`Async`** + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `args` | [`IBaseArgs`](../interfaces/IBaseArgs.md) | The arguments containing information about scripts. | + +#### Returns + +`Promise`<`Script`\> + +- Returns a MintingPolicy. + +#### Defined in + +[taste-test/src/lib/classes/TasteTest.class.ts:576](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/taste-test/src/lib/classes/TasteTest.class.ts#L576) + +___ + +### getNodeValidatorFromArgs + +▸ `Private` **getNodeValidatorFromArgs**(`args`): `Promise`<`Script`\> + +Retrieves the node validator based on the provided arguments. +It either fetches the validator directly from the arguments or derives it from a transaction hash. + +**`Throws`** + +Will throw an error if unable to derive UTXO from the provided OutRef in `scripts.validator`. + +**`Async`** + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `args` | [`IBaseArgs`](../interfaces/IBaseArgs.md) | The arguments containing information about scripts. | + +#### Returns + +`Promise`<`Script`\> + +- Returns a SpendingValidator. + +#### Defined in + +[taste-test/src/lib/classes/TasteTest.class.ts:541](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/taste-test/src/lib/classes/TasteTest.class.ts#L541) + +___ + +### getUserKey + +▸ `Private` **getUserKey**(): `Promise`<`string`\> + +Retrieves the user key (hash) from a given Cardano address. + +This method processes the Cardano address, attempts to extract the stake credential or payment credential hash, +and returns it as the user's key. If neither stake nor payment credentials are found, an error is thrown. + +It utilizes the `lucid.utils.getAddressDetails` method to parse and extract details from the Cardano address. + +**`Throws`** + +If neither stake nor payment credentials could be determined from the address. + +#### Returns + +`Promise`<`string`\> + +- The user key hash extracted from the address. + +#### Defined in + +[taste-test/src/lib/classes/TasteTest.class.ts:611](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/taste-test/src/lib/classes/TasteTest.class.ts#L611) + +___ + +### update + +▸ **update**(`args`): `Promise`<[`ITxBuilder`](../interfaces/ITxBuilder.md)<`Tx`, `undefined` \| `string`\>\> + +Initiates an update transaction for a node's assets, ensuring various checks and constructing the transaction. + +This method is responsible for initiating an update operation for a node. It starts by retrieving the node validator +and determining the user's key. It then searches for the user's own node, either based on provided UTXOs or by querying +the blockchain, and throws an error if the node isn't found or doesn't have a datum. + +Upon successfully identifying the node, the method prepares the redeemer for the node validator action and recalculates +the node's assets based on the new input amount. It then constructs a transaction that collects from the identified node +and repays it to the contract with updated assets. + +The transaction is assembled with appropriate components and redeemer information, ensuring the updated assets are correctly +set and the transaction is valid. The method completes the transaction by applying the necessary fees and preparing it for +submission. + +**`Async`** + +**`Throws`** + +Throws an error if the user's payment credential hash is missing or if the node with the required datum cannot be found. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `args` | [`IUpdateArgs`](../interfaces/IUpdateArgs.md) | The arguments required for the update operation, including potential UTXOs and the amount to add. | + +#### Returns + +`Promise`<[`ITxBuilder`](../interfaces/ITxBuilder.md)<`Tx`, `undefined` \| `string`\>\> + +- Returns a promise that resolves with a transaction builder object, +equipped with the transaction, its associated fees, and functions to build, sign, and submit the transaction. + +#### Implementation of + +[AbstractTasteTest](AbstractTasteTest.md).[update](AbstractTasteTest.md#update) + +#### Defined in + +[taste-test/src/lib/classes/TasteTest.class.ts:243](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/taste-test/src/lib/classes/TasteTest.class.ts#L243) + +___ + +### withdraw + +▸ **withdraw**(`args`): `Promise`<[`ITxBuilder`](../interfaces/ITxBuilder.md)<`Tx`, `undefined` \| `string`\>\> + +Processes a withdrawal transaction, handling various pre-conditions and state checks. + +This method is responsible for orchestrating a withdrawal operation, including validating nodes, +ensuring proper policy adherence, and constructing the necessary transaction steps. It checks +for the user's participation in the network, retrieves the associated validator and policy information, +and determines the rightful ownership of assets. + +Depending on the transaction's timing relative to certain deadlines, different transaction paths may be taken. +This could involve simply completing the transaction or applying a penalty in certain conditions. The method +handles these variations and constructs the appropriate transaction type. + +After preparing the necessary information and constructing the transaction steps, it completes the transaction +by setting the appropriate fees and preparing it for submission. + +**`Async`** + +**`Throws`** + +Throws errors if the withdrawal conditions are not met, such as missing keys, inability to find nodes, or ownership issues. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `args` | [`IWithdrawArgs`](../interfaces/IWithdrawArgs.md) | The required arguments for the withdrawal operation. | + +#### Returns + +`Promise`<[`ITxBuilder`](../interfaces/ITxBuilder.md)<`Tx`, `undefined` \| `string`\>\> + +- Returns a promise that resolves with a transaction builder object, which includes the transaction, its associated fees, and functions to build, sign, and submit the transaction. + +#### Implementation of + +[AbstractTasteTest](AbstractTasteTest.md).[withdraw](AbstractTasteTest.md#withdraw) + +#### Defined in + +[taste-test/src/lib/classes/TasteTest.class.ts:315](https://github.com/SundaeSwap-finance/sundae-sdk/blob/main/packages/taste-test/src/lib/classes/TasteTest.class.ts#L315) diff --git a/docs/typescript/taste-test/interfaces/IBaseArgs.md b/docs/typescript/taste-test/interfaces/IBaseArgs.md new file mode 100644 index 00000000..08d8df0c --- /dev/null +++ b/docs/typescript/taste-test/interfaces/IBaseArgs.md @@ -0,0 +1,11 @@ +# Interface: IBaseArgs + +Common arguments for the deposit and update methods of the TasteTest class instance. + +## Hierarchy + +- **`IBaseArgs`** + + ↳ [`IDepositArgs`](IDepositArgs.md) + + ↳ [`IWithdrawArgs`](IWithdrawArgs.md) diff --git a/docs/typescript/taste-test/interfaces/IDepositArgs.md b/docs/typescript/taste-test/interfaces/IDepositArgs.md new file mode 100644 index 00000000..51c178eb --- /dev/null +++ b/docs/typescript/taste-test/interfaces/IDepositArgs.md @@ -0,0 +1,11 @@ +# Interface: IDepositArgs + +Arguments for the deposit method of the TasteTest class instance. + +## Hierarchy + +- [`IBaseArgs`](IBaseArgs.md) + + ↳ **`IDepositArgs`** + + ↳↳ [`IUpdateArgs`](IUpdateArgs.md) diff --git a/docs/typescript/taste-test/interfaces/ITasteTestCompleteTxArgs.md b/docs/typescript/taste-test/interfaces/ITasteTestCompleteTxArgs.md new file mode 100644 index 00000000..154abeef --- /dev/null +++ b/docs/typescript/taste-test/interfaces/ITasteTestCompleteTxArgs.md @@ -0,0 +1,3 @@ +# Interface: ITasteTestCompleteTxArgs + +Object arguments for completing a transaction. diff --git a/docs/typescript/taste-test/interfaces/ITxBuilder.md b/docs/typescript/taste-test/interfaces/ITxBuilder.md new file mode 100644 index 00000000..f0f023fb --- /dev/null +++ b/docs/typescript/taste-test/interfaces/ITxBuilder.md @@ -0,0 +1,10 @@ +# Interface: ITxBuilder + +The primary top-level API surface for dealing with built TxBuilder transactions. + +## Type parameters + +| Name | Type | +| :------ | :------ | +| `T` | `unknown` | +| `K` | `unknown` | diff --git a/docs/typescript/taste-test/interfaces/IUpdateArgs.md b/docs/typescript/taste-test/interfaces/IUpdateArgs.md new file mode 100644 index 00000000..667b269a --- /dev/null +++ b/docs/typescript/taste-test/interfaces/IUpdateArgs.md @@ -0,0 +1,9 @@ +# Interface: IUpdateArgs + +Arguments for the update method of the TasteTest class instance. + +## Hierarchy + +- [`IDepositArgs`](IDepositArgs.md) + + ↳ **`IUpdateArgs`** diff --git a/docs/typescript/taste-test/interfaces/IWithdrawArgs.md b/docs/typescript/taste-test/interfaces/IWithdrawArgs.md new file mode 100644 index 00000000..d645b5c0 --- /dev/null +++ b/docs/typescript/taste-test/interfaces/IWithdrawArgs.md @@ -0,0 +1,9 @@ +# Interface: IWithdrawArgs + +Arguments for the deposit withdraw of the TasteTest class instance. + +## Hierarchy + +- [`IBaseArgs`](IBaseArgs.md) + + ↳ **`IWithdrawArgs`** diff --git a/docs/typescript/taste-test/modules.md b/docs/typescript/taste-test/modules.md index 6900cf42..a8552072 100644 --- a/docs/typescript/taste-test/modules.md +++ b/docs/typescript/taste-test/modules.md @@ -54,3 +54,17 @@ if (walletInstance) { tt?.deposit({ ...args }); ``` + +## Classes + +- [AbstractTasteTest](classes/AbstractTasteTest.md) +- [TasteTest](classes/TasteTest.md) + +## Interfaces + +- [IBaseArgs](interfaces/IBaseArgs.md) +- [IDepositArgs](interfaces/IDepositArgs.md) +- [ITasteTestCompleteTxArgs](interfaces/ITasteTestCompleteTxArgs.md) +- [ITxBuilder](interfaces/ITxBuilder.md) +- [IUpdateArgs](interfaces/IUpdateArgs.md) +- [IWithdrawArgs](interfaces/IWithdrawArgs.md) diff --git a/jest.config.ts b/jest.config.ts index afcaaf3f..ba0f6403 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -3,9 +3,9 @@ import { JestConfigWithTsJest } from "ts-jest"; const config: JestConfigWithTsJest = { projects: [ { - displayName: "@sundaeswap/core", + displayName: "All Packages", testEnvironment: "node", - testMatch: ["/packages/core/**/__tests__/**/*.test.*?(x)"], + testMatch: ["/packages/(?!demo)*/**/__tests__/**/*.test.*?(x)"], testPathIgnorePatterns: ["/dist/", "/node_modules/", "__tests__/data"], extensionsToTreatAsEsm: [".ts"], transform: { diff --git a/packages/core/src/@types/queryprovider.ts b/packages/core/src/@types/queryprovider.ts index 20bd0d12..028985fc 100644 --- a/packages/core/src/@types/queryprovider.ts +++ b/packages/core/src/@types/queryprovider.ts @@ -4,6 +4,14 @@ import { UTXO } from "./datumbuilder"; +/** + * Defines the type of pool list to retrieve. + */ +export enum EPoolSearchType { + ALL = "pools", + POPULAR = "poolsPopular", +} + /** * The base Provider interface by which you can implement custom Provider classes. * @@ -15,6 +23,7 @@ export interface IQueryProviderClass { * * @param pair An array of the hex-encoded assetIDs in the pool, separated by a period (i.e. POLICY_ID.ASSET_NAME). * @param fee A string representation of the pool fee, as a float (i.e. "0.03"). + * @returns {Promise} Returns the queried pool's data. */ findPoolData: (query: IPoolQuery) => Promise; @@ -23,9 +32,19 @@ export interface IQueryProviderClass { * * @param pair An array of the hex-encoded assetIDs in the pool, separated by a period (i.e. POLICY_ID.ASSET_NAME). * @param fee A string representation of the pool fee, as a float (i.e. "0.03"). + * @returns {Promise} Returns queried pool's ident. */ findPoolIdent: (query: IPoolQuery) => Promise; + /** + * Retrieves all available pools' data. + * + * @param type The type of search to perform. + * @param query A string to use as your query. + * @returns {Promise} Returns an array of IPoolData objects, each representing the data for an individual pool. + */ + getAllPools: (type?: EPoolSearchType, query?: string) => Promise; + /** * Finds the associated UTXO data of an open order. * diff --git a/packages/core/src/classes/Extensions/QueryProviders/QueryProvider.SundaeSwap.ts b/packages/core/src/classes/Extensions/QueryProviders/QueryProvider.SundaeSwap.ts index ee9694b3..ab107da8 100644 --- a/packages/core/src/classes/Extensions/QueryProviders/QueryProvider.SundaeSwap.ts +++ b/packages/core/src/classes/Extensions/QueryProviders/QueryProvider.SundaeSwap.ts @@ -1,4 +1,5 @@ import { + EPoolSearchType, IPoolData, IPoolQuery, IQueryProviderClass, @@ -41,6 +42,51 @@ export class QueryProviderSundaeSwap implements IQueryProviderClass { return data.ident; } + async getAllPools( + type: EPoolSearchType = EPoolSearchType.POPULAR, + query: string = "" + ): Promise { + const res: { + data?: { + pools: IPoolData[]; + }; + } = await fetch(this.baseUrl, { + method: "POST", + body: JSON.stringify({ + query: ` + query allPools($query: String!) { + ${type}(${query ? "query: $query, " : ""}pageSize: 200) { + fee + ident + assetA { + assetId + decimals + } + assetB { + assetId + decimals + } + quantityA + quantityB + } + } + `, + variables: { + query, + }, + }), + }).then((res) => res.json()); + + if (!res?.data) { + throw new Error( + "Something went wrong when trying to fetch the pool list. Full response: " + + JSON.stringify(res) + ); + } + + return res.data.pools; + } + async findPoolData({ pair: [coinA, coinB], fee }: IPoolQuery) { const res: { data?: { diff --git a/packages/demo/src/components/Actions/modules/DepositTasteTest.tsx b/packages/demo/src/components/Actions/modules/DepositTasteTest.tsx index dd1111ee..4b510807 100644 --- a/packages/demo/src/components/Actions/modules/DepositTasteTest.tsx +++ b/packages/demo/src/components/Actions/modules/DepositTasteTest.tsx @@ -30,6 +30,7 @@ export const DepositTasteTest: FC = ({ try { await tt .deposit({ + updateFallback: true, assetAmount: new AssetAmount(1000000n, 6), scripts: { policy: { diff --git a/packages/taste-test/src/@types/index.ts b/packages/taste-test/src/@types/index.ts index df335ff6..ff6a3696 100644 --- a/packages/taste-test/src/@types/index.ts +++ b/packages/taste-test/src/@types/index.ts @@ -7,6 +7,9 @@ import type { UTxO, } from "lucid-cardano"; +/** + * Common arguments for the deposit and update methods of the TasteTest class instance. + */ export interface IBaseArgs { currentTime?: number; referralFee?: ITxBuilderReferralFee; @@ -17,14 +20,24 @@ export interface IBaseArgs { utxos?: UTxO[]; } +/** + * Arguments for the deposit method of the TasteTest class instance. + */ export interface IDepositArgs extends IBaseArgs { assetAmount: AssetAmount; + updateFallback?: boolean; } +/** + * Arguments for the update method of the TasteTest class instance. + */ export interface IUpdateArgs extends IDepositArgs { assetAmount: AssetAmount; } +/** + * Arguments for the deposit withdraw of the TasteTest class instance. + */ export interface IWithdrawArgs extends IBaseArgs { deadline: number; penaltyAddress: string; diff --git a/packages/taste-test/src/index.ts b/packages/taste-test/src/index.ts index beb6ee69..b4b02c31 100644 --- a/packages/taste-test/src/index.ts +++ b/packages/taste-test/src/index.ts @@ -57,4 +57,8 @@ * @module TasteTest * @packageDocumentation */ -export { TasteTest } from "./lib/classes/TasteTest.class"; +export type { ITxBuilder } from "@sundaeswap/core"; +export * from "./@types/index"; +export * from "./lib/classes/AbstractTasteTest.class"; +export * from "./lib/classes/TasteTest.class"; +export * from "./utils"; diff --git a/packages/taste-test/src/lib/classes/AbstractTasteTest.class.ts b/packages/taste-test/src/lib/classes/AbstractTasteTest.class.ts index fa596007..ab3f98e0 100644 --- a/packages/taste-test/src/lib/classes/AbstractTasteTest.class.ts +++ b/packages/taste-test/src/lib/classes/AbstractTasteTest.class.ts @@ -3,10 +3,53 @@ import type { Lucid } from "lucid-cardano"; import { IDepositArgs, IUpdateArgs, IWithdrawArgs } from "../../@types"; +/** + * Represents the abstract class that should be extended to implement + * the functionality of the Taste Test features. This class provides + * the structure for depositing, updating, and withdrawing operations. + * + * @property {Lucid} lucid - An instance of the Lucid class, representing the core functionality handler. + */ export abstract class AbstractTasteTest { + /** + * Represents the Lucid instance used for core operations. + */ abstract lucid: Lucid; + /** + * Initiates a deposit transaction. The specific implementation of this method + * should handle the business logic associated with making a deposit, including + * validations, transaction building, and error handling. + * + * @param {IDepositArgs} args - The arguments required for the deposit operation, + * including the amount, user credentials, and other transaction details. + * @returns {Promise} - Returns a promise that resolves with an + * ITxBuilder instance, representing the constructed transaction for the deposit. + */ abstract deposit(args: IDepositArgs): Promise; + + /** + * Initiates an update transaction. This method is responsible for handling + * the business logic necessary to update an existing record or transaction. + * This could include changing the amount, modifying references, or other updates. + * + * @param {IUpdateArgs} args - The arguments required for the update operation. + * This includes any fields that are updatable within the transaction and may + * include credentials for authorization. + * @returns {Promise} - Returns a promise that resolves with an + * ITxBuilder instance, representing the constructed transaction for the update. + */ abstract update(args: IUpdateArgs): Promise; + + /** + * Initiates a withdrawal transaction. This method should handle the logic + * associated with withdrawing funds, including validations, constructing the + * withdrawal transaction, and handling errors appropriately. + * + * @param {IWithdrawArgs} args - The arguments required for the withdrawal operation, + * including the amount to withdraw, user credentials, and other necessary details. + * @returns {Promise} - Returns a promise that resolves with an + * ITxBuilder instance, representing the constructed transaction for the withdrawal. + */ abstract withdraw(args: IWithdrawArgs): Promise; } diff --git a/packages/taste-test/src/lib/classes/TasteTest.class.ts b/packages/taste-test/src/lib/classes/TasteTest.class.ts index 518d08e7..8b69f735 100644 --- a/packages/taste-test/src/lib/classes/TasteTest.class.ts +++ b/packages/taste-test/src/lib/classes/TasteTest.class.ts @@ -38,7 +38,7 @@ import { import { FOLDING_FEE_ADA, MIN_COMMITMENT_ADA, - NODE_ADA, + NODE_DEPOSIT_ADA, SETNODE_PREFIX, TIME_TOLERANCE_MS, TWENTY_FOUR_HOURS_MS, @@ -48,21 +48,73 @@ import { AbstractTasteTest } from "./AbstractTasteTest.class"; /** * Object arguments for completing a transaction. */ -interface ITasteTestCompleteTxArgs { +export interface ITasteTestCompleteTxArgs { tx: Tx; referralFee?: ITxBuilderReferralFee; - datum?: string; - complete?: boolean; + hasFees?: boolean; } +/** + * Represents the TasteTest class capable of handling various blockchain interactions for a protocol Taste Test. + * + * This class encapsulates methods for common operations required in the Taste Test process on the blockchain. + * It provides an interface to deposit assets, withdraw them, and update the commitment amounts. Each operation + * is transaction-based and interacts with blockchain smart contracts to ensure the correct execution of business logic. + * + * The TasteTest class relies on the Lucid service, which can come either from your own instance or from an existing + * SundaeSDK class instance (as shown below). The class methods handle the detailed steps of each operation, including + * transaction preparation, fee handling, and error checking, abstracting these complexities from the end user. + * + * Usage example (not part of the actual documentation): + * ```ts + * import type { Lucid } from "lucid-cardano"; + * const tasteTest = new TasteTest(sdk.build().wallet); + * + * // For depositing assets into the taste test smart contract + * tasteTest.deposit({ ... }).then(({ build, fees }) => console.log(fees)); + * + * // For withdrawing assets from the taste test smart contract + * tasteTest.withdraw({ ... }).then(({ build, fees }) => console.log(fees));; + * + * // For updating the committed assets in the taste test smart contract + * tasteTest.update({ ... }).then(({ build, fees }) => console.log(fees));; + * ``` + * + * @public + * @class + * @implements {AbstractTasteTest} + * @param {Lucid} lucid - An instance of the Lucid class, providing various utility methods for blockchain interactions. + */ export class TasteTest implements AbstractTasteTest { constructor(public lucid: Lucid) {} + /** + * Initiates a deposit transaction, conducting various checks and orchestrating the transaction construction. + * + * This method is in charge of initiating a deposit operation. It first verifies the availability of UTXOs in the wallet + * and checks for the presence of necessary scripts. It retrieves the node validator and policy, determines the user's key, + * and identifies the covering node based on the provided or found UTXOs. + * + * If a covering node is not identified, the method checks whether the user already owns a node and, based on the `updateFallback` + * flag, either updates the node or throws an error. After identifying the covering node, it prepares the necessary data structures + * and transaction components, including the redeemer policy and validator actions. + * + * The transaction is then constructed, incorporating various elements such as assets, validators, and minting policies. + * It ensures the transaction falls within a valid time frame and completes it by setting the appropriate fees and preparing + * it for submission. + * + * @public + * @param {IDepositArgs} args - The required arguments for the deposit operation. + * @returns {Promise>} - Returns a promise that resolves with a transaction builder object, + * which includes the transaction, its associated fees, and functions to build, sign, and submit the transaction. + * @async + * @throws {Error} Throws an error if no UTXOs are available, if reference scripts are missing, or if a covering node cannot be found. + */ public async deposit( args: IDepositArgs ): Promise> { const walletUtxos = await this.lucid.wallet.getUtxos(); - const walletAddress = await this.lucid.wallet.address(); + const { updateFallback = false } = args; if (!walletUtxos.length) { throw new Error("No available UTXOs found in wallet."); @@ -80,22 +132,23 @@ export class TasteTest implements AbstractTasteTest { const nodePolicy = await this.getNodePolicyFromArgs(args); const nodePolicyId = this.lucid.utils.mintingPolicyToId(nodePolicy); - const userKey = - this.lucid.utils.getAddressDetails(walletAddress).paymentCredential?.hash; - - if (!userKey) { - throw new Error("Missing wallet's payment credential hash."); - } + const userKey = await this.getUserKey(); let coveringNode: UTxO | undefined; + const nodeUTXOs = await this.lucid.utxosAt(nodeValidatorAddr); + if (args.utxos) { coveringNode = args.utxos[0]; } else { - const nodeUTXOs = await this.lucid.utxosAt(nodeValidatorAddr); coveringNode = findCoveringNode(nodeUTXOs, userKey); } if (!coveringNode || !coveringNode.datum) { + const hasOwnNode = findOwnNode(nodeUTXOs, userKey); + if (hasOwnNode && updateFallback) { + return this.update({ ...args }); + } + throw new Error("Could not find covering node."); } @@ -158,19 +211,43 @@ export class TasteTest implements AbstractTasteTest { .validFrom(lowerBound) .validTo(upperBound); - return this.completeTx({ tx, referralFee: args.referralFee }); + return this.completeTx({ + tx, + referralFee: args.referralFee, + hasFees: true, + }); } + /** + * Initiates an update transaction for a node's assets, ensuring various checks and constructing the transaction. + * + * This method is responsible for initiating an update operation for a node. It starts by retrieving the node validator + * and determining the user's key. It then searches for the user's own node, either based on provided UTXOs or by querying + * the blockchain, and throws an error if the node isn't found or doesn't have a datum. + * + * Upon successfully identifying the node, the method prepares the redeemer for the node validator action and recalculates + * the node's assets based on the new input amount. It then constructs a transaction that collects from the identified node + * and repays it to the contract with updated assets. + * + * The transaction is assembled with appropriate components and redeemer information, ensuring the updated assets are correctly + * set and the transaction is valid. The method completes the transaction by applying the necessary fees and preparing it for + * submission. + * + * @public + * @param {IUpdateArgs} args - The arguments required for the update operation, including potential UTXOs and the amount to add. + * @returns {Promise>} - Returns a promise that resolves with a transaction builder object, + * equipped with the transaction, its associated fees, and functions to build, sign, and submit the transaction. + * @async + * @throws {Error} Throws an error if the user's payment credential hash is missing or if the node with the required datum cannot be found. + */ public async update( args: IUpdateArgs - ): Promise> { + ): Promise> { const nodeValidator = await this.getNodeValidatorFromArgs(args); const nodeValidatorAddr = this.lucid.utils.validatorToAddress(nodeValidator); - const userKey = this.lucid.utils.getAddressDetails( - await this.lucid.wallet.address() - ).paymentCredential?.hash; + const userKey = await this.getUserKey(); if (!userKey) { throw new Error("Missing wallet's payment credential hash."); @@ -214,6 +291,27 @@ export class TasteTest implements AbstractTasteTest { return this.completeTx({ tx, referralFee: args.referralFee }); } + /** + * Processes a withdrawal transaction, handling various pre-conditions and state checks. + * + * This method is responsible for orchestrating a withdrawal operation, including validating nodes, + * ensuring proper policy adherence, and constructing the necessary transaction steps. It checks + * for the user's participation in the network, retrieves the associated validator and policy information, + * and determines the rightful ownership of assets. + * + * Depending on the transaction's timing relative to certain deadlines, different transaction paths may be taken. + * This could involve simply completing the transaction or applying a penalty in certain conditions. The method + * handles these variations and constructs the appropriate transaction type. + * + * After preparing the necessary information and constructing the transaction steps, it completes the transaction + * by setting the appropriate fees and preparing it for submission. + * + * @public + * @param {IWithdrawArgs} args - The required arguments for the withdrawal operation. + * @returns {Promise>} - Returns a promise that resolves with a transaction builder object, which includes the transaction, its associated fees, and functions to build, sign, and submit the transaction. + * @async + * @throws {Error} Throws errors if the withdrawal conditions are not met, such as missing keys, inability to find nodes, or ownership issues. + */ public async withdraw( args: IWithdrawArgs ): Promise> { @@ -224,9 +322,7 @@ export class TasteTest implements AbstractTasteTest { const nodePolicy = await this.getNodePolicyFromArgs(args); const nodePolicyId = this.lucid.utils.mintingPolicyToId(nodePolicy); - const userKey = this.lucid.utils.getAddressDetails( - await this.lucid.wallet.address() - ).paymentCredential?.hash; + const userKey = await this.getUserKey(); if (!userKey) { throw new Error("Missing wallet's payment credential hash."); @@ -304,7 +400,6 @@ export class TasteTest implements AbstractTasteTest { ) .addSignerKey(userKey) .mintAssets(assets, redeemerNodePolicy) - // .attachMintingPolicy(nodePolicy) .compose(this.lucid.newTx().attachMintingPolicy(nodePolicy)) .validFrom(lowerBound) .validTo(upperBound); @@ -316,7 +411,6 @@ export class TasteTest implements AbstractTasteTest { const tx = this.lucid .newTx() .collectFrom([ownNode, prevNode], redeemerNodeValidator) - // .attachSpendingValidator(nodeValidator) .compose(this.lucid.newTx().attachSpendingValidator(nodeValidator)) .payToContract( nodeValidatorAddr, @@ -328,7 +422,6 @@ export class TasteTest implements AbstractTasteTest { }) .addSignerKey(userKey) .mintAssets(assets, redeemerNodePolicy) - // .attachMintingPolicy(nodePolicy) .compose(this.lucid.newTx().attachMintingPolicy(nodePolicy)) .validFrom(lowerBound) .validTo(upperBound); @@ -354,9 +447,33 @@ export class TasteTest implements AbstractTasteTest { return this.completeTx({ tx, referralFee: args.referralFee }); } + /** + * Finalizes the construction of a transaction with the necessary operations and fees. + * + * This function takes a constructed transaction and applies the final necessary steps to make it ready for submission. + * These steps include setting the referral fee if it's part of the transaction, calculating the necessary transaction fees, + * and preparing the transaction for signing. The function adapts to the presence of asset-specific transactions by + * handling different referral payment types. + * + * The base fees for the transaction are calculated based on a boolean for whether to include them, specifically for deposit and fold. + * The only fees that are always set are referral fees and the native Cardano transaction fee. + * + * Once the transaction is built, it's completed, and the actual Cardano network transaction fee is retrieved and set. + * The built transaction is then ready for signing and submission. + * + * @private + * @param {ITasteTestCompleteTxArgs} params - The arguments required for completing the transaction, including the transaction itself, the referral fee, and a flag indicating if the transaction includes fees. + * @param {boolean} params.hasFees - Indicates whether the transaction has fees associated with it. + * @param {IReferralFee | null} params.referralFee - The referral fee information, if applicable. + * @param {Tx} params.tx - The initial transaction object that needs to be completed. + * @returns {Promise>} - Returns a promise that resolves with a transaction builder object, which includes the transaction, its associated fees, and functions to build, sign, and submit the transaction. + * @async + * @throws {Error} Throws an error if the transaction cannot be completed or if there are issues with the fee calculation. + */ private async completeTx({ - tx, + hasFees = false, referralFee, + tx, }: ITasteTestCompleteTxArgs): Promise> { if (referralFee) { if (referralFee.payment.metadata.assetId !== "") { @@ -371,9 +488,9 @@ export class TasteTest implements AbstractTasteTest { } const baseFees: Omit = { - deposit: new AssetAmount(NODE_ADA, 6), + deposit: new AssetAmount(hasFees ? NODE_DEPOSIT_ADA : 0n, 6), referral: referralFee?.payment, - foldFee: new AssetAmount(FOLDING_FEE_ADA, 6), + foldFee: new AssetAmount(hasFees ? FOLDING_FEE_ADA : 0n, 6), scooperFee: new AssetAmount(0n), }; @@ -386,12 +503,13 @@ export class TasteTest implements AbstractTasteTest { async build() { if (!finishedTx) { finishedTx = await tx.complete(); - thisTx.fees.cardanoTxFee = new AssetAmount( - BigInt(txFee?.to_str() ?? finishedTx?.fee?.toString() ?? "0"), - 6 - ); } + thisTx.fees.cardanoTxFee = new AssetAmount( + BigInt(txFee?.to_str() ?? finishedTx?.fee?.toString() ?? "0"), + 6 + ); + return { cbor: Buffer.from(finishedTx.txComplete.to_bytes()).toString("hex"), sign: async () => { @@ -420,10 +538,12 @@ export class TasteTest implements AbstractTasteTest { * @async * @private */ - private async getNodeValidatorFromArgs({ + public async getNodeValidatorFromArgs({ scripts, }: IBaseArgs): Promise { let nodeValidator: SpendingValidator; + + // If we're using a txHash property, then we know to fetch the UTXO, otherwise, we already have the data. if ("txHash" in scripts.validator) { const script = ( await this.lucid.provider.getUtxosByOutRef([scripts.validator]) @@ -453,10 +573,12 @@ export class TasteTest implements AbstractTasteTest { * @async * @private */ - private async getNodePolicyFromArgs({ + public async getNodePolicyFromArgs({ scripts, }: IBaseArgs): Promise { let nodePolicy: MintingPolicy; + + // If we're using a txHash property, then we know to fetch the UTXO, otherwise, we already have the data. if ("txHash" in scripts.policy) { const script = ( await this.lucid.provider.getUtxosByOutRef([scripts.policy]) @@ -473,4 +595,30 @@ export class TasteTest implements AbstractTasteTest { return nodePolicy; } + + /** + * Retrieves the user key (hash) from a given Cardano address. + * + * This method processes the Cardano address, attempts to extract the stake credential or payment credential hash, + * and returns it as the user's key. If neither stake nor payment credentials are found, an error is thrown. + * + * It utilizes the `lucid.utils.getAddressDetails` method to parse and extract details from the Cardano address. + * + * @private + * @returns {Promise} - The user key hash extracted from the address. + * @throws {Error} If neither stake nor payment credentials could be determined from the address. + */ + public async getUserKey(): Promise { + const address = await this.lucid.wallet.address(); + const details = this.lucid.utils.getAddressDetails(address); + const userKey = + details?.stakeCredential?.hash ?? details?.paymentCredential?.hash; + if (!userKey) { + throw new Error( + `Could not determine the key hash of the user's address: ${address}` + ); + } + + return userKey; + } } diff --git a/packages/taste-test/src/lib/contants.ts b/packages/taste-test/src/lib/contants.ts index 4a459565..a7135fea 100644 --- a/packages/taste-test/src/lib/contants.ts +++ b/packages/taste-test/src/lib/contants.ts @@ -1,24 +1,9 @@ -import { fromText } from "lucid-cardano"; - export const SETNODE_PREFIX = "FSN"; -export const CORRNODE_PREFIX = "FCN"; -export const CFOLD = "CFold"; -export const PTHOLDER = "PTHolder"; export const TWENTY_FOUR_HOURS_MS = 86_400_000; -export const ONE_HOUR_MS = 3_600_000; - -export const originNodeTokenName = fromText(SETNODE_PREFIX); -export const corrNodeTokenName = fromText(CORRNODE_PREFIX); -export const cFold = fromText(CFOLD); -export const NODE_ADA = 3_000_000n; +export const NODE_DEPOSIT_ADA = 3_000_000n; export const FOLDING_FEE_ADA = 1_000_000n; -export const MIN_COMMITMENT_ADA = NODE_ADA + FOLDING_FEE_ADA; +export const MIN_COMMITMENT_ADA = NODE_DEPOSIT_ADA + FOLDING_FEE_ADA; export const TIME_TOLERANCE_MS = process.env.NODE_ENV == "emulator" ? 0 : 100_000; - -export const PROTOCOL_PAYMENT_KEY = - "014e9d57e1623f7eeef5d0a8d4e6734a562ba32cf910244cd74e1680"; -export const PROTOCOL_STAKE_KEY = - "5e8aa3f089868eaadf188426f49db6566624844b6c5d529b38f3b8a7"; diff --git a/packages/taste-test/typedoc.json b/packages/taste-test/typedoc.json index c6154cce..397860af 100644 --- a/packages/taste-test/typedoc.json +++ b/packages/taste-test/typedoc.json @@ -2,7 +2,7 @@ "$schema": "https://typedoc.org/schema.json", "out": "../../docs/typescript/taste-test", "entryPoints": [ - "./src/index.ts", + "./src/index.ts" ], "hideBreadcrumbs": true, "hideInPageTOC": true, From 25e866d95f3b82f466b5557f24726265902f0a82 Mon Sep 17 00:00:00 2001 From: Calvin Date: Fri, 20 Oct 2023 14:32:08 -0600 Subject: [PATCH 3/5] Update packages/taste-test/src/utils.ts Co-authored-by: Selvio Perez --- packages/taste-test/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/taste-test/src/utils.ts b/packages/taste-test/src/utils.ts index c4ed3cd9..70b88997 100644 --- a/packages/taste-test/src/utils.ts +++ b/packages/taste-test/src/utils.ts @@ -45,7 +45,7 @@ export const findOwnNode = (utxos: UTxO[], userKey: string) => } const nodeData = Data.from(value.datum, SetNode); - return nodeData.key == userKey; + return nodeData.key === userKey; }); /** From 6f549a2c5b7f48aca2618ef372e83eace2e2b995 Mon Sep 17 00:00:00 2001 From: Calvin Date: Fri, 20 Oct 2023 14:32:54 -0600 Subject: [PATCH 4/5] Update packages/demo/src/components/Actions/modules/WithdrawTasteTest.tsx Co-authored-by: Selvio Perez --- .../Actions/modules/WithdrawTasteTest.tsx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/demo/src/components/Actions/modules/WithdrawTasteTest.tsx b/packages/demo/src/components/Actions/modules/WithdrawTasteTest.tsx index d09ca7ec..3a285d7a 100644 --- a/packages/demo/src/components/Actions/modules/WithdrawTasteTest.tsx +++ b/packages/demo/src/components/Actions/modules/WithdrawTasteTest.tsx @@ -45,18 +45,16 @@ export const WithdrawTasteTest: FC = ({ outputIndex: 0, }, }, - ...(useReferral - ? { - referralFee: { - destination: - "addr_test1qp6crwxyfwah6hy7v9yu5w6z2w4zcu53qxakk8ynld8fgcpxjae5d7xztgf0vyq7pgrrsk466xxk25cdggpq82zkpdcsdkpc68", - payment: new AssetAmount(1000000n, { - assetId: "", - decimals: 6, - }), - }, - } - : {}), + ...(useReferral && { + referralFee: { + destination: + "addr_test1qp6crwxyfwah6hy7v9yu5w6z2w4zcu53qxakk8ynld8fgcpxjae5d7xztgf0vyq7pgrrsk466xxk25cdggpq82zkpdcsdkpc68", + payment: new AssetAmount(1000000n, { + assetId: "", + decimals: 6, + }), + }, + }), }) .then(async ({ build, fees }) => { setFees(fees); From 8822ec361d639de3eda2176ccc704f32bf435330 Mon Sep 17 00:00:00 2001 From: Calvin Date: Fri, 20 Oct 2023 14:33:51 -0600 Subject: [PATCH 5/5] Update packages/taste-test/src/utils.ts Co-authored-by: Selvio Perez --- packages/taste-test/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/taste-test/src/utils.ts b/packages/taste-test/src/utils.ts index 70b88997..6c820574 100644 --- a/packages/taste-test/src/utils.ts +++ b/packages/taste-test/src/utils.ts @@ -66,7 +66,7 @@ export const findPrevNode = (utxos: UTxO[], userKey: string) => } const datum = Data.from(value.datum, SetNode); - return datum.next !== null && datum.next == userKey; + return datum?.next === userKey; }); /**