diff --git a/changelog.d/20240511_113958_nicolas.henin_runtime_lifecycle_refactor.md b/changelog.d/20240511_113958_nicolas.henin_runtime_lifecycle_refactor.md new file mode 100644 index 00000000..80c0f2a7 --- /dev/null +++ b/changelog.d/20240511_113958_nicolas.henin_runtime_lifecycle_refactor.md @@ -0,0 +1,43 @@ +### General + +- Feat: **Initial Account Deposits Feature Integration (Runtime v1.0.0):** + + - **Purpose:** This update introduces the capability for users to make initial deposits into their accounts upon creation. This feature aims to streamline the account setup process and enhance user experience. + - **Benefits:** This feature squashes the Contract Creation and Initial Input Deposits into 1 transaction instead of multiple ones. + +- Feat: **Introduction of a New Contract API in the Runtime Lifecycle API:** + - **Purpose:** The addition of a new Contract API is designed to provide developers with more flexibility and control (contract instance concept) over smart contract management within the runtime environment. + - **Benefits:** Developers can now leverage enhanced functionalities for deploying, updating, and interacting with smart contracts. This API simplifies complex contract operations and supports more robust smart contract development. + +### @marlowe.io/runtime-rest-client + +- Feat: `initial account deposits` (runtime v1.0.0) for Contract Creation (`BuildCreateContractTxRequest` via `buildCreateContractTx`):([PR-188](https://github.com/input-output-hk/marlowe-ts-sdk/pull/188)) + +### @marlowe.io/runtime-core + +- Feat: `initial account deposits` (runtime v1.0.0) for Contract Creation ([PR-188](https://github.com/input-output-hk/marlowe-ts-sdk/pull/188)): + - Added `export type AccountDeposits = { [key in AddressOrRole]: AssetsMap };` and associated utility functions. + +### @marlowe.io/runtime-lifecycle + +- Feat: New Contract API `packages/runtime/lifecycle/src/generic/new-contract-api.ts` ([PR-188](https://github.com/input-output-hk/marlowe-ts-sdk/pull/188)): + - Generic `waitConfirmation()` : same for contract creation and apply inputs + - Seamless Integration of Applicable Actions API + - simplfied interface (`create` and `load` with a concept of `ContractInstance` object) + - see end-to-end tests for examples (e.g : `swap.ada.token.e2e.spec.ts`) +- Feat: `initial account deposits` feature (runtime v1.0.0) for Contract Creation ([PR-188](https://github.com/input-output-hk/marlowe-ts-sdk/pull/188)): + - new parameter field `accountDeposits` in + - e.g. + +```ts +const sellerContractInstance = await sellerLifecycle.newContractAPI.create({ + contract: swapContract, + roles: { [scheme.ask.buyer.role_token]: mintRole("OpenRole") }, + accountDeposits: mkaccountDeposits([[scheme.offer.seller, seller.assetsProvisioned]]), +}); +``` + +### @marlowe.io/language-examples + +- Feat: `Atomic swap v2` : Simplified version using the new runtime `v1.0.0` feature (`initial account deposits`) + - see end-to-end tests for examples (e.g : `swap.ada.token.e2e.spec.ts`) diff --git a/examples/nodejs/src/escrow-flow.ts b/examples/nodejs/src/escrow-flow.ts index c56e59d2..0b8f814a 100644 --- a/examples/nodejs/src/escrow-flow.ts +++ b/examples/nodejs/src/escrow-flow.ts @@ -88,15 +88,14 @@ async function main(action: "buy" | "sell", otherAddress: string, amount: number console.log("Mediator: " + Mediator); console.log("Amount: " + amount); - const [contractId, txId] = await runtime.contracts.createContract({ + const contractInstance = await runtime.newContractAPI.create({ contract: escrow, roles: { Buyer, Seller, Mediator }, }); - console.log("Contract ID: " + contractId); - console.log("Transaction ID: " + txId); + console.log("Contract ID: " + contractInstance.id); console.log("Waiting for confirmation..."); - await wallet.waitConfirmation(txId); + await contractInstance.waitForConfirmation(); console.log("Contract created successfully!"); } diff --git a/examples/nodejs/src/experimental-features/source-map.ts b/examples/nodejs/src/experimental-features/source-map.ts index 33ec47bc..d6de760f 100644 --- a/examples/nodejs/src/experimental-features/source-map.ts +++ b/examples/nodejs/src/experimental-features/source-map.ts @@ -1,7 +1,7 @@ import * as M from "fp-ts/lib/Map.js"; import { ContractBundleMap, bundleMapToList, isAnnotated, stripAnnotations } from "@marlowe.io/marlowe-object"; -import { CreateContractRequestBase, RuntimeLifecycle } from "@marlowe.io/runtime-lifecycle/api"; +import { ContractInstanceAPI, CreateContractRequestBase, RuntimeLifecycle } from "@marlowe.io/runtime-lifecycle/api"; import { ContractClosure, getContractClosure } from "./contract-closure.js"; import * as Core from "@marlowe.io/language-core-v1"; @@ -9,7 +9,12 @@ import * as CoreG from "@marlowe.io/language-core-v1/guards"; import * as Obj from "@marlowe.io/marlowe-object"; import * as ObjG from "@marlowe.io/marlowe-object/guards"; -import { SingleInputTx, TransactionOutput, playSingleInputTxTrace } from "@marlowe.io/language-core-v1/semantics"; +import { + SingleInputTx, + TransactionOutput, + emptyState, + playSingleInputTxTrace, +} from "@marlowe.io/language-core-v1/semantics"; import { RestClient } from "@marlowe.io/runtime-rest-client"; import { ContractId, TxId } from "@marlowe.io/runtime-core"; import { deepEqual } from "@marlowe.io/adapter/deep-equal"; @@ -154,7 +159,7 @@ export interface SourceMap { closure: ContractClosure; annotateHistory(history: SingleInputTx[]): SingleInputTx[]; playHistory(history: SingleInputTx[]): TransactionOutput; - createContract(options: CreateContractRequestBase): Promise<[ContractId, TxId]>; + createContract(options: CreateContractRequestBase): Promise; contractInstanceOf(contractId: ContractId): Promise; } @@ -173,11 +178,11 @@ export async function mkSourceMap( const annotatedHistory = annotateHistoryFromClosure(closure)(history); const main = closure.contracts.get(closure.main); if (typeof main === "undefined") throw new Error(`Cant find main.`); - return playSingleInputTxTrace(0n, main, annotatedHistory); + return playSingleInputTxTrace(emptyState(0n), main, annotatedHistory); }, createContract: (options: CreateContractRequestBase) => { const contract = stripAnnotations(closure.contracts.get(closure.main)!); - return lifecycle.contracts.createContract({ + return lifecycle.newContractAPI.create({ ...options, contract, }); diff --git a/examples/nodejs/src/marlowe-object-flow.ts b/examples/nodejs/src/marlowe-object-flow.ts index 5ba9378d..1541d963 100644 --- a/examples/nodejs/src/marlowe-object-flow.ts +++ b/examples/nodejs/src/marlowe-object-flow.ts @@ -11,6 +11,7 @@ */ import { mkLucidWallet, WalletAPI } from "@marlowe.io/wallet"; import { mkRuntimeLifecycle } from "@marlowe.io/runtime-lifecycle"; +import { ContractInstanceAPI } from "@marlowe.io/runtime-lifecycle/api"; import { Lucid, Blockfrost, C } from "lucid-cardano"; import { readConfig } from "./config.js"; import { datetoTimeout, When } from "@marlowe.io/language-core-v1"; @@ -18,6 +19,7 @@ import { addressBech32, contractId, ContractId, + contractIdToTxId, stakeAddressBech32, StakeAddressBech32, TxId, @@ -171,7 +173,7 @@ async function createContractMenu(lifecycle: RuntimeLifecycle, rewardAddress?: S }; const metadata = delayPaymentTemplate.toMetadata(scheme); const sourceMap = await mkSourceMap(lifecycle, mkDelayPayment(scheme)); - const [contractId, txId] = await sourceMap.createContract({ + const contractInstance = await sourceMap.createContract({ stakeAddress: rewardAddress, tags: { DELAY_PAYMENT_VERSION: "2" }, metadata, @@ -179,9 +181,9 @@ async function createContractMenu(lifecycle: RuntimeLifecycle, rewardAddress?: S console.log(`Contract created with id ${contractId}`); - await waitIndicator(lifecycle.wallet, txId); + await waitIndicator(lifecycle.wallet, contractIdToTxId(contractInstance.id)); - return contractMenu(lifecycle, scheme, sourceMap, contractId); + return contractMenu(lifecycle.wallet, contractInstance, scheme, sourceMap); } /** @@ -213,43 +215,39 @@ async function loadContractMenu(lifecycle: RuntimeLifecycle) { console.log(` * Amount: ${validationResult.scheme.amount} lovelaces`); console.log(` * Deposit deadline: ${validationResult.scheme.depositDeadline}`); console.log(` * Release deadline: ${validationResult.scheme.releaseDeadline}`); - - return contractMenu(lifecycle, validationResult.scheme, validationResult.sourceMap, cid); + const contractInstance = await lifecycle.newContractAPI.load(cid); + return contractMenu(lifecycle.wallet, contractInstance, validationResult.scheme, validationResult.sourceMap); } /** * This is an Inquirer.js flow to interact with a contract */ async function contractMenu( - lifecycle: RuntimeLifecycle, + wallet: WalletAPI, + contractInstance: ContractInstanceAPI, scheme: DelayPaymentParameters, - sourceMap: SourceMap, - contractId: ContractId + sourceMap: SourceMap ): Promise { // Get and print the contract logical state. - const inputHistory = await lifecycle.contracts.getInputHistory(contractId); + + const inputHistory = await contractInstance.getInputHistory(); const contractState = getState(datetoTimeout(new Date()), inputHistory, sourceMap); + if (contractState.type === "Closed") return; printState(contractState, scheme); - // See what actions are applicable to the current contract state - const { contractDetails, actions } = await lifecycle.applicableActions.getApplicableActions(contractId); - - if (contractDetails.type === "closed") return; - - const myActionsFilter = await lifecycle.applicableActions.mkFilter(contractDetails); - const myActions = actions.filter(myActionsFilter); + const applicableActions = await contractInstance.evaluateApplicableActions(); const choices: Array<{ name: string; - value: CanDeposit | CanAdvance | { actionType: "check-state" } | { actionType: "return" }; + value: CanDeposit | CanAdvance | { type: "check-state" } | { type: "return" }; }> = [ { name: "Re-check contract state", - value: { actionType: "check-state" }, + value: { type: "check-state" }, }, - ...myActions.map((action) => { - switch (action.actionType) { + ...applicableActions.myActions.map((action) => { + switch (action.type) { case "Advance": return { name: "Close contract", @@ -271,7 +269,7 @@ async function contractMenu( }), { name: "Return to main menu", - value: { actionType: "return" }, + value: { type: "return" }, }, ]; @@ -279,21 +277,21 @@ async function contractMenu( message: "Contract menu", choices, }); - switch (selectedAction.actionType) { + switch (selectedAction.type) { case "check-state": - return contractMenu(lifecycle, scheme, sourceMap, contractId); + return contractMenu(wallet, contractInstance, scheme, sourceMap); case "return": return; case "Advance": case "Deposit": console.log("Applying input"); - const applicableInput = await lifecycle.applicableActions.getInput(contractDetails, selectedAction); - const txId = await lifecycle.applicableActions.applyInput(contractId, { + const applicableInput = await applicableActions.toInput(selectedAction); + const txId = await applicableActions.apply({ input: applicableInput, }); console.log(`Input applied with txId ${txId}`); - await waitIndicator(lifecycle.wallet, txId); - return contractMenu(lifecycle, scheme, sourceMap, contractId); + await waitIndicator(wallet, txId); + return contractMenu(wallet, contractInstance, scheme, sourceMap); } } diff --git a/examples/rest-client-flow/index.html b/examples/rest-client-flow/index.html index 6ce76b04..bafe4be2 100644 --- a/examples/rest-client-flow/index.html +++ b/examples/rest-client-flow/index.html @@ -25,7 +25,7 @@

Request

This should be filled with a JSON object that starts with an array, where each element is a numbered parameter.

- +
@@ -148,7 +148,7 @@

Console

switch (action) { case "getContracts": log(`Getting contracts from ${H.getRuntimeUrl()}`); - result = await restClient.getContracts(...params); + result = await restClient.getContracts(params); console.log("Contracts", result); const nextRange = result.nextRange?.value ?? "-"; const prevRange = result.prevRange?.value ?? "-"; @@ -158,12 +158,12 @@

Console

break; case "submitContract": log(`Submitting contract on ${H.getRuntimeUrl()}`); - await restClient.submitContract(...getParams()); + await restClient.submitContract(getParams()); log(`Done`); break; default: log(`Calling ${action} on ${H.getRuntimeUrl()}`); - result = await restClient[action](...params); + result = await restClient[action](params); logJSON("Result:", result); } } catch (e) { @@ -182,10 +182,7 @@

Console

try { params = JSON.parse(jsonParams); } catch (e) { - throw new Error("Parameters must be a valid JSON array: " + e); - } - if (!Array.isArray(params)) { - throw new Error("Parameters must be an array"); + throw new Error("Parameters must be a valid JSON: " + e); } return params; } diff --git a/packages/adapter/src/io-ts.ts b/packages/adapter/src/io-ts.ts index 0f98c761..04399a5b 100644 --- a/packages/adapter/src/io-ts.ts +++ b/packages/adapter/src/io-ts.ts @@ -5,6 +5,8 @@ import { Errors } from "io-ts/lib/index.js"; import { Refinement } from "fp-ts/lib/Refinement.js"; import { pipe } from "fp-ts/lib/function.js"; import * as Either from "fp-ts/lib/Either.js"; +import * as BigIntReporter from "jsonbigint-io-ts-reporters"; + /** * In the TS-SDK we duplicate the type and guard definition for each type as the * inferred type from io-ts does not produce a good type export when used with @@ -88,20 +90,37 @@ export function expectType(guard: t.Type, aValue: unknown): T { } /** - * A mechanism for validating the type of a strict in a dynamically type context. - * @param strict Whether to perform runtime checking to provide helpful error messages. May have a slight negative performance impact. + * Formats validation errors into a string. + * @param errors - The validation errors to format. + * @returns A string representation of the validation errors. */ -export function strictDynamicTypeCheck(strict: unknown): strict is boolean { - return typeof strict === "boolean"; +export function formatValidationErrors(errors: Errors): string { + return BigIntReporter.formatValidationErrors(errors, { + truncateLongTypes: true, + }).join("\n"); +} + +export function dynamicAssertType(guard: G, value: unknown, message?: string): t.TypeOf { + const result = guard.decode(value); + if (Either.isLeft(result)) { + throw new InvalidTypeError(guard, value, result.left, message); + } + return result.right; } +/** + * This error is thrown when we are dynamicly checking for the type of a value and the type is not + * the expected one. + */ export class InvalidTypeError extends Error { constructor( + public readonly guard: t.Any, + public readonly value: unknown, public readonly errors: Errors, - public readonly value: any, message?: string ) { - super(message); + const msg = message ?? `Unexpected type for value:\n${formatValidationErrors(errors)}`; + super(msg); } } diff --git a/packages/adapter/src/time.ts b/packages/adapter/src/time.ts index 86a2ed5d..e9200dff 100644 --- a/packages/adapter/src/time.ts +++ b/packages/adapter/src/time.ts @@ -30,9 +30,6 @@ export const waitForPredicatePromise = async ( // Predicate is already true, no need to wait return; } - // Use a promise to wait for the specified interval - const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - // Wait for the specified interval await sleep(seconds); diff --git a/packages/language/core/v1/src/environment.ts b/packages/language/core/v1/src/environment.ts index 17c82b48..04783146 100644 --- a/packages/language/core/v1/src/environment.ts +++ b/packages/language/core/v1/src/environment.ts @@ -12,7 +12,7 @@ export const mkEnvironment = }); /** - * TODO: Comment + * Time interval in which the contract is executed. It is defined by a start and end time. The time is represented as a POSIX time. * @see Appendix E.16 of the {@link https://github.com/input-output-hk/marlowe/releases/download/v3/Marlowe.pdf | Marlowe specification} * @category Environment */ @@ -22,7 +22,7 @@ export interface TimeInterval { } /** - * TODO: Comment + * Guard for {@link TimeInterval} * @see Appendix E.16 of the {@link https://github.com/input-output-hk/marlowe/releases/download/v3/Marlowe.pdf | Marlowe specification} * @category Environment */ @@ -32,7 +32,7 @@ export const TimeIntervalGuard: t.Type = t.type({ }); /** - * TODO: Comment + * Time interval in which the contract is executed. * @see Section 2.1.10 and appendix E.22 of the {@link https://github.com/input-output-hk/marlowe/releases/download/v3/Marlowe.pdf | Marlowe specification} * @category Environment */ @@ -41,7 +41,7 @@ export interface Environment { } /** - * TODO: Comment + * Guard for {@link Environment} * @see Section 2.1.10 and appendix E.22 of the {@link https://github.com/input-output-hk/marlowe/releases/download/v3/Marlowe.pdf | Marlowe specification} * @category Environment */ diff --git a/packages/language/core/v1/src/index.ts b/packages/language/core/v1/src/index.ts index 3249920f..cbd6a881 100644 --- a/packages/language/core/v1/src/index.ts +++ b/packages/language/core/v1/src/index.ts @@ -83,7 +83,7 @@ export { MerkleizedNotify, } from "./inputs.js"; -export { role, Party, Address, Role, RoleName } from "./participants.js"; +export { role, Party, Address, Role, RoleName, partiesToStrings, partyToString } from "./participants.js"; export { Payee, PayeeAccount, PayeeParty, AccountId } from "./payee.js"; diff --git a/packages/language/core/v1/src/semantics.ts b/packages/language/core/v1/src/semantics.ts index 6fd4faee..c9c512e4 100644 --- a/packages/language/core/v1/src/semantics.ts +++ b/packages/language/core/v1/src/semantics.ts @@ -833,7 +833,11 @@ export function emptyState(minTime: POSIXTime): MarloweState { * @returns The resulting state and contract, along with the accumulated warnings and payments or a {@link TransactionError}. * @category Evaluation */ -export function playTrace(initialTime: POSIXTime, contract: Contract, transactions: Transaction[]): TransactionOutput { +export function playTrace( + initialState: MarloweState, + contract: Contract, + transactions: Transaction[] +): TransactionOutput { function go(prev: TransactionOutput, txs: Transaction[]): TransactionOutput { if (txs.length === 0) return prev; if (!G.TransactionSuccess.is(prev)) return prev; @@ -855,13 +859,13 @@ export function playTrace(initialTime: POSIXTime, contract: Contract, transactio warnings: [], payments: [], contract, - state: emptyState(initialTime), + state: initialState, }, transactions ); } -export function playSingleInputTxTrace(initialTime: POSIXTime, contract: Contract, transactions: SingleInputTx[]) { +export function playSingleInputTxTrace(initialState: MarloweState, contract: Contract, transactions: SingleInputTx[]) { const txs = transactions.map((tx) => { const tx_inputs = typeof tx.input === "undefined" ? [] : [tx.input]; return { @@ -869,5 +873,5 @@ export function playSingleInputTxTrace(initialTime: POSIXTime, contract: Contrac tx_inputs, }; }); - return playTrace(initialTime, contract, txs); + return playTrace(initialState, contract, txs); } diff --git a/packages/language/examples/src/atomicSwap.ts b/packages/language/examples/src/atomicSwap.ts index caf977b2..518c5e1a 100644 --- a/packages/language/examples/src/atomicSwap.ts +++ b/packages/language/examples/src/atomicSwap.ts @@ -10,7 +10,7 @@ * are timeboxed. Sellers are known at the contract creation (fixed Address) and Buyers are unknown * (This showcases a feature of marlowe that is called Open Roles.). * There are 3 main stages : - * - The Offer : The Sellers deposit their tokens. + * - The Offer : The Sellers create the contract and deposit their tokens at the same time via the initial account deposits feature. * - The Ask : The Buyers deposit their tokens * - The Swap Confirmation : an extra Notify input is added after the swap to avoid double-satisfaction attack (see link attached). * (Any third participant could perform this action) @@ -54,19 +54,18 @@ * @packageDocumentation */ +import { POSIXTime } from "@marlowe.io/adapter/time"; import { Address, Contract, IChoice, IDeposit, INotify, - Input, MarloweState, Role, Timeout, TokenValue, close, - datetoTimeout, } from "@marlowe.io/language-core-v1"; import * as G from "@marlowe.io/language-core-v1/guards"; import { SingleInputTx } from "@marlowe.io/language-core-v1/semantics"; @@ -96,29 +95,14 @@ export type Scheme = { /* #region State */ export type State = ActiveState | Closed; -export type ActiveState = WaitingSellerOffer | NoSellerOfferInTime | WaitingForAnswer | WaitingForSwapConfirmation; - -export type WaitingSellerOffer = { - typeName: "WaitingSellerOffer"; -}; - -export const waitingSellerOffer: WaitingSellerOffer = { - typeName: "WaitingSellerOffer", -}; - -export type NoSellerOfferInTime = { - typeName: "NoSellerOfferInTime"; -}; -export const noSellerOfferInTime: NoSellerOfferInTime = { - typeName: "NoSellerOfferInTime", -}; +export type ActiveState = WaitingForAnswer | WaitingForSwapConfirmation; export type WaitingForAnswer = { - typeName: "WaitingForAnswer"; + type: "WaitingForAnswer"; }; export const waitingForAnswer: WaitingForAnswer = { - typeName: "WaitingForAnswer", + type: "WaitingForAnswer", }; /* @@ -134,18 +118,18 @@ export const waitingForAnswer: WaitingForAnswer = { *

*/ export type WaitingForSwapConfirmation = { - typeName: "WaitingForSwapConfirmation"; + type: "WaitingForSwapConfirmation"; }; export const waitingForSwapConfirmation: WaitingForSwapConfirmation = { - typeName: "WaitingForSwapConfirmation", + type: "WaitingForSwapConfirmation", }; /** * when the contract is closed. */ export type Closed = { - typeName: "Closed"; + type: "Closed"; reason: CloseReason; }; @@ -153,11 +137,7 @@ export type Closed = { /** * Action List available for the contract lifecycle. */ -export type Action = - /* When Contract Created (timed out > NoOfferProvisionnedOnTime) */ - | ProvisionOffer // > OfferProvisionned - /* When NoOfferProvisionnedOnTime (timed out > no timeout (need to be reduced to be closed))*/ - | RetrieveMinimumLovelaceAdded // > closed +export type ApplicableAction = /* When OfferProvisionned (timed out > NotConfirmedOnTime) */ | Retract // > closed | Swap // > Swapped @@ -166,31 +146,20 @@ export type Action = export type ActionParticipant = "buyer" | "seller" | "anybody"; -export type RetrieveMinimumLovelaceAdded = { - typeName: "RetrieveMinimumLovelaceAdded"; - owner: ActionParticipant; -}; - -export type ProvisionOffer = { - typeName: "ProvisionOffer"; - owner: ActionParticipant; - input: IDeposit; -}; - export type Swap = { - typeName: "Swap"; + type: "Swap"; owner: ActionParticipant; input: IDeposit; }; export type ConfirmSwap = { - typeName: "ConfirmSwap"; + type: "ConfirmSwap"; owner: ActionParticipant; input: INotify; }; export type Retract = { - typeName: "Retract"; + type: "Retract"; owner: ActionParticipant; input: IChoice; }; @@ -198,22 +167,14 @@ export type Retract = { /* #endregion */ /* #region Close Reason */ -export type CloseReason = - | NoOfferProvisionnedOnTime - | SellerRetracted - | NotAnsweredOnTime - | Swapped - | SwappedButNotNotifiedOnTime; +export type CloseReason = SellerRetracted | NotAnsweredOnTime | Swapped | SwappedButNotNotifiedOnTime; -export type NoOfferProvisionnedOnTime = { - typeName: "NoOfferProvisionnedOnTime"; -}; -export type SellerRetracted = { typeName: "SellerRetracted" }; -export type NotAnsweredOnTime = { typeName: "NotAnsweredOnTime" }; +export type SellerRetracted = { type: "SellerRetracted" }; +export type NotAnsweredOnTime = { type: "NotAnsweredOnTime" }; export type SwappedButNotNotifiedOnTime = { - typeName: "SwappedButNotNotifiedOnTime"; + type: "SwappedButNotNotifiedOnTime"; }; -export type Swapped = { typeName: "Swapped" }; +export type Swapped = { type: "Swapped" }; /* #endregion */ @@ -241,32 +202,12 @@ export class UnexpectedClosedSwapContractState extends Error { } } -export const getAvailableActions = (scheme: Scheme, state: ActiveState): Action[] => { - switch (state.typeName) { - case "WaitingSellerOffer": - return [ - { - typeName: "ProvisionOffer", - owner: "seller", - input: { - input_from_party: scheme.offer.seller, - that_deposits: scheme.offer.asset.amount, - of_token: scheme.offer.asset.token, - into_account: scheme.offer.seller, - }, - }, - ]; - case "NoSellerOfferInTime": - return [ - { - typeName: "RetrieveMinimumLovelaceAdded", - owner: "anybody", - }, - ]; +export const getApplicableActions = (scheme: Scheme, state: ActiveState): ApplicableAction[] => { + switch (state.type) { case "WaitingForAnswer": return [ { - typeName: "Swap", + type: "Swap", owner: "buyer", input: { input_from_party: scheme.ask.buyer, @@ -276,7 +217,7 @@ export const getAvailableActions = (scheme: Scheme, state: ActiveState): Action[ }, }, { - typeName: "Retract", + type: "Retract", owner: "seller", input: { for_choice_id: { @@ -290,7 +231,7 @@ export const getAvailableActions = (scheme: Scheme, state: ActiveState): Action[ case "WaitingForSwapConfirmation": return [ { - typeName: "ConfirmSwap", + type: "ConfirmSwap", owner: "anybody", input: "input_notify", }, @@ -307,39 +248,34 @@ export const getClosedState = (scheme: Scheme, inputHistory: SingleInputTx[]): C // Offer Provision Deadline has passed and there is one reduced applied to close the contract case 0: return { - typeName: "Closed", - reason: { typeName: "NoOfferProvisionnedOnTime" }, + type: "Closed", + reason: { type: "NotAnsweredOnTime" }, }; - case 1: - return { - typeName: "Closed", - reason: { typeName: "NotAnsweredOnTime" }, - }; - case 2: { + case 1: { const isRetracted = - G.IChoice.is(inputHistory[1].input) && inputHistory[1].input.for_choice_id.choice_name == "retract"; + G.IChoice.is(inputHistory[0].input) && inputHistory[0].input.for_choice_id.choice_name == "retract"; const nbDeposits = inputHistory.filter((singleInputTx) => G.IDeposit.is(singleInputTx.input)).length; - if (isRetracted && nbDeposits === 1) { + if (isRetracted) { return { - typeName: "Closed", - reason: { typeName: "SellerRetracted" }, + type: "Closed", + reason: { type: "SellerRetracted" }, }; } - if (nbDeposits === 2) { + if (nbDeposits === 1) { return { - typeName: "Closed", - reason: { typeName: "SwappedButNotNotifiedOnTime" }, + type: "Closed", + reason: { type: "SwappedButNotNotifiedOnTime" }, }; } break; } - case 3: { + case 2: { const nbDeposits = inputHistory.filter((singleInputTx) => G.IDeposit.is(singleInputTx.input)).length; const nbNotify = inputHistory.filter((singleInputTx) => G.INotify.is(singleInputTx.input)).length; - if (nbDeposits === 2 && nbNotify === 1) { + if (nbDeposits === 1 && nbNotify === 1) { return { - typeName: "Closed", - reason: { typeName: "Swapped" }, + type: "Closed", + reason: { type: "Swapped" }, }; } } @@ -355,16 +291,14 @@ export const getActiveState = ( ): ActiveState => { switch (inputHistory.length) { case 0: - return now < scheme.offer.deadline ? { typeName: "WaitingSellerOffer" } : { typeName: "NoSellerOfferInTime" }; - case 1: if (now < scheme.ask.deadline) { - return { typeName: "WaitingForAnswer" }; + return { type: "WaitingForAnswer" }; } break; - case 2: { + case 1: { const nbDeposits = inputHistory.filter((singleInputTx) => G.IDeposit.is(singleInputTx.input)).length; - if (nbDeposits === 2 && now < scheme.swapConfirmation.deadline) { - return { typeName: "WaitingForSwapConfirmation" }; + if (nbDeposits === 1 && now < scheme.swapConfirmation.deadline) { + return { type: "WaitingForSwapConfirmation" }; } break; } @@ -373,22 +307,26 @@ export const getActiveState = ( throw new UnexpectedActiveSwapContractState(scheme, inputHistory, state); }; -export function mkContract(scheme: Scheme): Contract { - const mkOffer = (ask: Contract): Contract => { - const depositOffer = { - party: scheme.offer.seller, - deposits: scheme.offer.asset.amount, - of_token: scheme.offer.asset.token, - into_account: scheme.offer.seller, - }; - - return { - when: [{ case: depositOffer, then: ask }], - timeout: scheme.offer.deadline, - timeout_continuation: close, - }; +/** + * Generate the initial state of the contract from a given scheme and start date. + * @param startDate : the start date of the contract creation + * @param scheme : the scheme of the contract + * @returns + */ +export const mkInitialMarloweState = (startDate: POSIXTime, scheme: Scheme): MarloweState => { + return { + accounts: [[[scheme.offer.seller, scheme.offer.asset.token], scheme.offer.asset.amount]], + boundValues: [], + choices: [], + minTime: startDate, }; +}; +/** + * Generate the contract from the scheme. + * N.B : The offer asset is provisioned at the contract creation via initial account deposits. + */ +export function mkContract(scheme: Scheme): Contract { const mkAsk = (confirmSwap: Contract): Contract => { const depositAsk = { party: scheme.ask.buyer, @@ -450,5 +388,5 @@ export function mkContract(scheme: Scheme): Contract { }; }; - return mkOffer(mkAsk(mkSwapConfirmation())); + return mkAsk(mkSwapConfirmation()); } diff --git a/packages/language/examples/test/atomicSwap.spec.ts b/packages/language/examples/test/atomicSwap.spec.ts index f052bc67..1d3598d1 100644 --- a/packages/language/examples/test/atomicSwap.spec.ts +++ b/packages/language/examples/test/atomicSwap.spec.ts @@ -3,20 +3,18 @@ import * as G from "@marlowe.io/language-core-v1/guards"; import { expectType } from "@marlowe.io/adapter/io-ts"; import { datetoTimeout, close, Party, Payee, Input } from "@marlowe.io/language-core-v1"; -import { TransactionSuccess, emptyState, playTrace } from "@marlowe.io/language-core-v1/semantics"; +import { playTrace } from "@marlowe.io/language-core-v1/semantics"; import { ConfirmSwap, - ProvisionOffer, Retract, Swap, getActiveState, - getAvailableActions, + getApplicableActions, getClosedState, + mkInitialMarloweState, waitingForAnswer, waitingForSwapConfirmation, - waitingSellerOffer, } from "../src/atomicSwap.js"; -import * as t from "io-ts/lib/index.js"; const aDeadlineInThePast = datetoTimeout(new Date("2000-05-01")); const contractStart = datetoTimeout(new Date("2000-05-02")); @@ -38,8 +36,8 @@ const anotherAsset = { }; describe("Atomic Swap", () => { - describe("is active (on 4 different states)", () => { - it("when waiting a seller offer - WaitingSellerOffer", () => { + describe("is active (on 2 different states)", () => { + it("when waiting a for an answer - WaitingForAnswer", () => { // Set up const scheme: AtomicSwap.Scheme = { offer: { @@ -59,98 +57,16 @@ describe("Atomic Swap", () => { const inputFlow: Input[] = []; // Execute - - const contract = AtomicSwap.mkContract(scheme); - const state = emptyState(aDeadlineInThePast); + const state = mkInitialMarloweState(contractStart, scheme); // Verify - const activeState = getActiveState( scheme, aGivenNow, inputFlow.map((input) => ({ interval: aTxInterval, input: input })), state ); - expect(activeState.typeName).toBe("WaitingSellerOffer"); - }); - it("when no seller offer have been provided in time - NoSellerOfferInTime", () => { - // Set up - const scheme: AtomicSwap.Scheme = { - offer: { - seller: { address: "sellerAddress" }, - deadline: aDeadlineInTheFuture, - asset: anAsset, - }, - ask: { - buyer: { role_token: "buyer" }, - deadline: aDeadlineInTheFuture, - asset: anotherAsset, - }, - swapConfirmation: { - deadline: aDeadlineInTheFuture, - }, - }; - const inputFlow: Input[] = []; - - // Execute - - const contract = AtomicSwap.mkContract(scheme); - const state = emptyState(aDeadlineInThePast); - - // Verify - - const activeState = getActiveState( - scheme, - aGivenNowAfterDeadline, - inputFlow.map((input) => ({ interval: aTxInterval, input: input })), - state - ); - expect(activeState.typeName).toBe("NoSellerOfferInTime"); - }); - it("when waiting a for an answer - WaitingForAnswer", () => { - // Set up - const scheme: AtomicSwap.Scheme = { - offer: { - seller: { address: "sellerAddress" }, - deadline: aDeadlineInTheFuture, - asset: anAsset, - }, - ask: { - buyer: { role_token: "buyer" }, - deadline: aDeadlineInTheFuture, - asset: anotherAsset, - }, - swapConfirmation: { - deadline: aDeadlineInTheFuture, - }, - }; - const inputFlow: Input[] = [(getAvailableActions(scheme, waitingSellerOffer)[0] as ProvisionOffer).input]; - - // Execute - - const { contract, state, payments, warnings } = expectType( - G.TransactionSuccess, - playTrace(contractStart, AtomicSwap.mkContract(scheme), [ - { - tx_interval: aTxInterval, - tx_inputs: inputFlow, - }, - ]) - ); - - // Verify - - expect(warnings).toStrictEqual([]); - expect(contract).not.toBe(close); - expect(payments).toStrictEqual([]); - - const activeState = getActiveState( - scheme, - aGivenNow, - inputFlow.map((input) => ({ interval: aTxInterval, input: input })), - state - ); - expect(activeState.typeName).toBe("WaitingForAnswer"); + expect(activeState.type).toBe("WaitingForAnswer"); }); it("when waiting a for a swap confirmation (Open Role requirement to prevent double-satisfaction attacks) - WaitingForSwapConfirmation", () => { // Set up @@ -169,16 +85,13 @@ describe("Atomic Swap", () => { deadline: aDeadlineInTheFuture, }, }; - const inputFlow: Input[] = [ - (getAvailableActions(scheme, waitingSellerOffer)[0] as ProvisionOffer).input, - (getAvailableActions(scheme, waitingForAnswer)[0] as Swap).input, - ]; + const inputFlow: Input[] = [(getApplicableActions(scheme, waitingForAnswer)[0] as Swap).input]; // Execute const { contract, state, payments, warnings } = expectType( G.TransactionSuccess, - playTrace(contractStart, AtomicSwap.mkContract(scheme), [ + playTrace(mkInitialMarloweState(contractStart, scheme), AtomicSwap.mkContract(scheme), [ { tx_interval: aTxInterval, tx_inputs: inputFlow, @@ -211,7 +124,7 @@ describe("Atomic Swap", () => { inputFlow.map((input) => ({ interval: aTxInterval, input: input })), state ); - expect(activeState.typeName).toBe("WaitingForSwapConfirmation"); + expect(activeState.type).toBe("WaitingForSwapConfirmation"); }); }); describe("is closed (with 5 closed reasons)", () => { @@ -233,16 +146,15 @@ describe("Atomic Swap", () => { }, }; const inputFlow = [ - (getAvailableActions(scheme, waitingSellerOffer)[0] as ProvisionOffer).input, - (getAvailableActions(scheme, waitingForAnswer)[0] as Swap).input, - (getAvailableActions(scheme, waitingForSwapConfirmation)[0] as ConfirmSwap).input, + (getApplicableActions(scheme, waitingForAnswer)[0] as Swap).input, + (getApplicableActions(scheme, waitingForSwapConfirmation)[0] as ConfirmSwap).input, ]; // Execute const { contract, payments, warnings } = expectType( G.TransactionSuccess, - playTrace(contractStart, AtomicSwap.mkContract(scheme), [ + playTrace(mkInitialMarloweState(contractStart, scheme), AtomicSwap.mkContract(scheme), [ { tx_interval: aTxInterval, tx_inputs: inputFlow, @@ -286,7 +198,7 @@ describe("Atomic Swap", () => { scheme, inputFlow.map((input) => ({ interval: aTxInterval, input: input })) ); - expect(state.reason.typeName).toBe("Swapped"); + expect(state.reason.type).toBe("Swapped"); }); it("when tokens have been swapped but nobody has confirmed the swap on time (Open Role requirement to prevent double-satisfaction attacks) - SwappedButNotNotifiedOnTime", () => { // Set up @@ -305,16 +217,13 @@ describe("Atomic Swap", () => { deadline: aDeadlineInThePast, }, }; - const inputFlow = [ - (getAvailableActions(scheme, waitingSellerOffer)[0] as ProvisionOffer).input, - (getAvailableActions(scheme, waitingForAnswer)[0] as Swap).input, - ]; + const inputFlow = [(getApplicableActions(scheme, waitingForAnswer)[0] as Swap).input]; // Execute const { contract, payments, warnings } = expectType( G.TransactionSuccess, - playTrace(contractStart, AtomicSwap.mkContract(scheme), [ + playTrace(mkInitialMarloweState(contractStart, scheme), AtomicSwap.mkContract(scheme), [ { tx_interval: aTxInterval, tx_inputs: inputFlow, @@ -359,7 +268,7 @@ describe("Atomic Swap", () => { scheme, inputFlow.map((input) => ({ interval: aTxInterval, input: input })) ); - expect(state.reason.typeName).toBe("SwappedButNotNotifiedOnTime"); + expect(state.reason.type).toBe("SwappedButNotNotifiedOnTime"); }); it("when no buyer has answered to the offer on time - NotAnsweredOnTime", () => { // Set up @@ -378,16 +287,15 @@ describe("Atomic Swap", () => { deadline: aDeadlineInTheFuture, }, }; - const inputFlow = [(getAvailableActions(scheme, waitingSellerOffer)[0] as ProvisionOffer).input]; // Execute const { contract, payments, warnings } = expectType( G.TransactionSuccess, - playTrace(contractStart, AtomicSwap.mkContract(scheme), [ + playTrace(mkInitialMarloweState(contractStart, scheme), AtomicSwap.mkContract(scheme), [ { tx_interval: aTxInterval, - tx_inputs: inputFlow, + tx_inputs: [], }, ]) ); @@ -406,11 +314,8 @@ describe("Atomic Swap", () => { expect(contract).toBe(close); expect(payments).toStrictEqual(expectedPayments); - const state = getClosedState( - scheme, - inputFlow.map((input) => ({ interval: aTxInterval, input: input })) - ); - expect(state.reason.typeName).toBe("NotAnsweredOnTime"); + const state = getClosedState(scheme, []); + expect(state.reason.type).toBe("NotAnsweredOnTime"); }); it("when the seller has retracted - SellerRetracted", () => { // Set up @@ -429,16 +334,13 @@ describe("Atomic Swap", () => { deadline: aDeadlineInTheFuture, }, }; - const inputFlow = [ - (getAvailableActions(scheme, waitingSellerOffer)[0] as ProvisionOffer).input, - (getAvailableActions(scheme, waitingForAnswer)[1] as Retract).input, - ]; + const inputFlow = [(getApplicableActions(scheme, waitingForAnswer)[1] as Retract).input]; // Execute const { contract, payments, warnings } = expectType( G.TransactionSuccess, - playTrace(contractStart, AtomicSwap.mkContract(scheme), [ + playTrace(mkInitialMarloweState(contractStart, scheme), AtomicSwap.mkContract(scheme), [ { tx_interval: aTxInterval, tx_inputs: inputFlow, @@ -464,50 +366,7 @@ describe("Atomic Swap", () => { scheme, inputFlow.map((input) => ({ interval: aTxInterval, input: input })) ); - expect(state.reason.typeName).toBe("SellerRetracted"); - }); - it("when the seller has not provisioned the contract on time and advance is applied - NoOfferProvisionnedOnTime", () => { - // Set up - const scheme: AtomicSwap.Scheme = { - offer: { - seller: { address: "sellerAddress" }, - deadline: aDeadlineInThePast, - asset: anAsset, - }, - ask: { - buyer: { role_token: "buyer" }, - deadline: aDeadlineInTheFuture, - asset: anotherAsset, - }, - swapConfirmation: { - deadline: aDeadlineInTheFuture, - }, - }; - const advance: Input[] = []; - - // Execute - - const { contract, payments, warnings } = expectType( - G.TransactionSuccess, - playTrace(contractStart, AtomicSwap.mkContract(scheme), [ - { - tx_interval: aTxInterval, - tx_inputs: advance, - }, - ]) - ); - - // Verify - - expect(warnings).toStrictEqual([]); - expect(contract).toBe(close); - expect(payments).toStrictEqual([]); - - const state = getClosedState( - scheme, - advance.map((input) => ({ interval: aTxInterval, input: input })) - ); - expect(state.reason.typeName).toBe("NoOfferProvisionnedOnTime"); + expect(state.reason.type).toBe("SellerRetracted"); }); }); }); diff --git a/packages/language/specification-client/src/main.ts b/packages/language/specification-client/src/main.ts index b2d630f6..e40c9849 100644 --- a/packages/language/specification-client/src/main.ts +++ b/packages/language/specification-client/src/main.ts @@ -7,7 +7,13 @@ import * as P from "@marlowe.io/language-core-v1/playground-v1"; import jsonBigInt from "json-bigint"; import { createJsonStream } from "./jsonStream.js"; import { Environment, MarloweState, Value } from "@marlowe.io/language-core-v1"; -import { evalValue, evalObservation, computeTransaction, playTrace } from "@marlowe.io/language-core-v1/semantics"; +import { + evalValue, + evalObservation, + computeTransaction, + playTrace, + emptyState, +} from "@marlowe.io/language-core-v1/semantics"; // // We need to patch the JSON.stringify in order for BigInt serialization to work. const { stringify, parse } = jsonBigInt({ useNativeBigInt: true, @@ -147,7 +153,7 @@ function main() { return requestResponse(computeTransaction(req.transactionInput, req.state, req.coreContract)); } if (PlayTraceRequest.is(req)) { - return requestResponse(playTrace(req.initialTime, req.coreContract, req.transactionInputs)); + return requestResponse(playTrace(emptyState(req.initialTime), req.coreContract, req.transactionInputs)); } return console.log("RequestNotImplemented"); }, diff --git a/packages/runtime/client/rest/src/contract/endpoints/collection.ts b/packages/runtime/client/rest/src/contract/endpoints/collection.ts index b0b41173..745cac37 100644 --- a/packages/runtime/client/rest/src/contract/endpoints/collection.ts +++ b/packages/runtime/client/rest/src/contract/endpoints/collection.ts @@ -35,6 +35,8 @@ import { AddressBech32Guard, ContractId, ContractIdGuard, + AccountDeposits, + AccountDepositsGuard, } from "@marlowe.io/runtime-core"; import { ContractHeader, ContractHeaderGuard } from "../header.js"; import { RolesConfiguration, RolesConfigurationGuard } from "../rolesConfigurations.js"; @@ -191,7 +193,7 @@ export type BuildCreateContractTxRequestWithContract = { export const BuildCreateContractTxRequestOptionsGuard = assertGuardEqual( proxy(), t.intersection([ - t.type({ changeAddress: AddressBech32Guard, version: MarloweVersion }), + t.type({ changeAddress: AddressBech32Guard, version: MarloweVersion, accounts: AccountDepositsGuard }), t.partial({ roles: RolesConfigurationGuard, threadRoleName: G.RoleName, @@ -492,6 +494,12 @@ export interface BuildCreateContractTxRequestOptions { */ roles?: RolesConfiguration; + /** + * describe an initial deposit of assets for the accounts in the contract. The key is the address or role name and the value is the assets. + * These assets will be deposited in the accounts at the creation of the contract, and will come from the contract creator's wallet. + */ + accounts: AccountDeposits; + /** * The Marlowe validator version to use. */ @@ -517,6 +525,7 @@ export const PostContractsRequest = t.intersection([ contract: ContractOrSourceIdGuard, tags: TagsGuard, metadata: MetadataGuard, + accounts: AccountDepositsGuard, }), t.partial({ roles: RolesConfigurationGuard }), t.partial({ threadTokenName: G.RoleName }), diff --git a/packages/runtime/client/rest/src/index.ts b/packages/runtime/client/rest/src/index.ts index 01cd697a..25732fb6 100644 --- a/packages/runtime/client/rest/src/index.ts +++ b/packages/runtime/client/rest/src/index.ts @@ -31,7 +31,7 @@ import { ContractDetails } from "./contract/details.js"; import { TransactionDetails } from "./contract/transaction/details.js"; import { RuntimeStatus, healthcheck } from "./runtime/status.js"; import { CompatibleRuntimeVersionGuard, RuntimeVersion } from "./runtime/version.js"; -import { InvalidTypeError, strictDynamicTypeCheck } from "@marlowe.io/adapter/io-ts"; +import { dynamicAssertType } from "@marlowe.io/adapter/io-ts"; export { Page, ItemRange, ItemRangeGuard, ItemRangeBrand, PageGuard } from "./pagination.js"; @@ -213,43 +213,31 @@ export interface RestClient { getPayoutById(request: Payout.GetPayoutByIdRequest): Promise; } -function mkRestClientArgumentDynamicTypeCheck(baseURL: unknown, strict: boolean): baseURL is string { - return strict ? typeof baseURL === "string" : true; -} - -function withDynamicTypeCheck( - arg: any, - decode: (x: any) => t.Validation, +function withDynamicTypeCheck( strict: boolean, - cont: (x: T) => G -): G { - if (strict) { - const result = decode(arg); - if (result._tag === "Left") throw new InvalidTypeError(result.left, arg, "Invalid argument"); - } - return cont(arg); + requestGuard: t.Type, + doRequest: (x: Request) => Response +) { + return (request: unknown): Response => { + if (strict) { + const validatedRequest = dynamicAssertType(requestGuard, request); + + return doRequest(validatedRequest); + } else { + return doRequest(request as Request); + } + }; } -/** - * Instantiates a REST client for the Marlowe API. - * @param baseURL An http url pointing to the Marlowe API. - * @see {@link https://github.com/input-output-hk/marlowe-starter-kit#quick-overview} To get a Marlowe runtime instance up and running. - */ -export function mkRestClient(baseURL: string): RestClient; /** * Instantiates a REST client for the Marlowe API. * @param baseURL An http url pointing to the Marlowe API. * @param strict Whether to perform runtime checking to provide helpful error messages. May have a slight negative performance impact. Default value is `true`. * @see {@link https://github.com/input-output-hk/marlowe-starter-kit#quick-overview} To get a Marlowe runtime instance up and running. */ -export function mkRestClient(baseURL: string, strict: boolean): RestClient; -export function mkRestClient(baseURL: unknown, strict: unknown = true): RestClient { - if (!strictDynamicTypeCheck(strict)) { - throw new InvalidTypeError([], `Invalid type for argument 'strict', expected boolean but got ${strict}`); - } - if (!mkRestClientArgumentDynamicTypeCheck(baseURL, strict)) { - throw new InvalidTypeError([], `Invalid type for argument 'baseURL', expected string but got ${baseURL}`); - } +export function mkRestClient(baseURL: string, strict = true): RestClient { + dynamicAssertType(t.boolean, strict, "Strict should be a boolean"); + dynamicAssertType(t.string, baseURL, "Base URL should be a string"); const axiosInstance = axios.create({ baseURL, @@ -271,280 +259,168 @@ export function mkRestClient(baseURL: unknown, strict: unknown = true): RestClie version() { return healthcheck(axiosInstance).then((status) => status.version); }, - getContracts(request) { - return withDynamicTypeCheck( - request, - (x) => Contracts.GetContractsRequestGuard.decode(x), - strict, - (request) => { - const range = request?.range; - const tags = request?.tags ?? []; - const partyAddresses = request?.partyAddresses ?? []; - const partyRoles = request?.partyRoles ?? []; - return unsafeTaskEither( - Contracts.getHeadersByRangeViaAxios(axiosInstance)(range)({ - tags, - partyAddresses, - partyRoles, - }) - ); - } + getContracts: withDynamicTypeCheck(strict, Contracts.GetContractsRequestGuard, (request) => { + const range = request?.range; + const tags = request?.tags ?? []; + const partyAddresses = request?.partyAddresses ?? []; + const partyRoles = request?.partyRoles ?? []; + return unsafeTaskEither( + Contracts.getHeadersByRangeViaAxios(axiosInstance)(range)({ + tags, + partyAddresses, + partyRoles, + }) ); - }, - getContractById(request) { - return withDynamicTypeCheck( - request, - (x) => Contract.GetContractByIdRequest.decode(x), - strict, - (request) => { - return Contract.getContractById(axiosInstance, request.contractId); - } + }), + getContractById: withDynamicTypeCheck(strict, Contract.GetContractByIdRequest, (request) => { + return Contract.getContractById(axiosInstance, request.contractId); + }), + buildCreateContractTx: withDynamicTypeCheck( + strict, + Contracts.BuildCreateContractTxRequestGuard, + async (request) => { + const version = await runtimeVersion; + // NOTE: Runtime 0.0.5 requires an explicit minUTxODeposit, but 0.0.6 and forward allows that field as optional + // and it will calculate the actual minimum required. We use the version of the runtime to determine + // if we use a "safe" default that is bigger than needed. + const minUTxODeposit = request.minimumLovelaceUTxODeposit ?? (version === "0.0.5" ? 3000000 : undefined); + const postContractsRequest = { + contract: "contract" in request ? request.contract : request.sourceId, + version: request.version, + metadata: request.metadata ?? {}, + tags: request.tags ?? {}, + minUTxODeposit, + roles: request.roles, + threadRoleName: request.threadRoleName, + accounts: request.accounts ?? {}, + }; + const addressesAndCollaterals = { + changeAddress: request.changeAddress, + usedAddresses: request.usedAddresses ?? [], + collateralUTxOs: request.collateralUTxOs ?? [], + }; + return unsafeTaskEither( + Contracts.postViaAxios(axiosInstance)(postContractsRequest, addressesAndCollaterals, request.stakeAddress) + ); + } + ), + createContractSources: withDynamicTypeCheck(strict, Sources.CreateContractSourcesRequestGuard, (request) => { + const { + bundle: { main, bundle }, + } = request; + return Sources.createContractSources(axiosInstance)(main, bundle); + }), + getContractSourceById: withDynamicTypeCheck(strict, Sources.GetContractBySourceIdRequestGuard, (request) => { + return Sources.getContractSourceById(axiosInstance)(request); + }), + getContractSourceAdjacency: withDynamicTypeCheck( + strict, + Sources.GetContractSourceAdjacencyRequestGuard, + (request) => { + return Sources.getContractSourceAdjacency(axiosInstance)(request); + } + ), + getContractSourceClosure: withDynamicTypeCheck(strict, Sources.GetContractSourceClosureRequestGuard, (request) => { + return Sources.getContractSourceClosure(axiosInstance)(request); + }), + getNextStepsForContract: withDynamicTypeCheck(strict, Next.GetNextStepsForContractRequestGuard, (request) => { + return Next.getNextStepsForContract(axiosInstance)(request); + }), + submitContract: withDynamicTypeCheck(strict, Contract.SubmitContractRequestGuard, (request) => { + const { contractId, txEnvelope } = request; + return Contract.submitContract(axiosInstance)(contractId, txEnvelope); + }), + getTransactionsForContract: withDynamicTypeCheck( + strict, + Transactions.GetTransactionsForContractRequestGuard, + (request) => { + const { contractId, range } = request; + return unsafeTaskEither(Transactions.getHeadersByRangeViaAxios(axiosInstance)(contractId, range)); + } + ), + submitContractTransaction: withDynamicTypeCheck( + strict, + Transaction.SubmitContractTransactionRequestGuard, + (request) => { + const { contractId, transactionId, hexTransactionWitnessSet } = request; + return unsafeTaskEither( + Transaction.putViaAxios(axiosInstance)(contractId, transactionId, hexTransactionWitnessSet) + ); + } + ), + getContractTransactionById: withDynamicTypeCheck( + strict, + Transaction.GetContractTransactionByIdRequestGuard, + (request) => { + const { contractId, txId } = request; + return unsafeTaskEither(Transaction.getViaAxios(axiosInstance)(contractId, txId)); + } + ), + withdrawPayouts: withDynamicTypeCheck(strict, Withdrawals.WithdrawPayoutsRequestGuard, (request) => { + const { payoutIds, changeAddress } = request; + return unsafeTaskEither( + Withdrawals.postViaAxios(axiosInstance)(payoutIds, { + changeAddress, + usedAddresses: request.usedAddresses ?? [], + collateralUTxOs: request.collateralUTxOs ?? [], + }) ); - }, - async buildCreateContractTx(request) { - return withDynamicTypeCheck( - request, - (x) => Contracts.BuildCreateContractTxRequestGuard.decode(x), - strict, - async (request) => { - const version = await runtimeVersion; - // NOTE: Runtime 0.0.5 requires an explicit minUTxODeposit, but 0.0.6 and forward allows that field as optional - // and it will calculate the actual minimum required. We use the version of the runtime to determine - // if we use a "safe" default that is bigger than needed. - const minUTxODeposit = request.minimumLovelaceUTxODeposit ?? (version === "0.0.5" ? 3000000 : undefined); - const postContractsRequest = { - contract: "contract" in request ? request.contract : request.sourceId, - version: request.version, + }), + getWithdrawalById: withDynamicTypeCheck(strict, Withdrawal.GetWithdrawalByIdRequestGuard, async (request) => { + const { withdrawalId } = request; + const { block, ...response } = await unsafeTaskEither(Withdrawal.getViaAxios(axiosInstance)(withdrawalId)); + return { ...response, block: O.toUndefined(block) }; + }), + getWithdrawals: withDynamicTypeCheck(strict, Withdrawals.GetWithdrawalsRequestGuard, (request) => { + return unsafeTaskEither(Withdrawals.getHeadersByRangeViaAxios(axiosInstance)(request)); + }), + applyInputsToContract: withDynamicTypeCheck(strict, Transactions.ApplyInputsToContractRequestGuard, (request) => { + const { contractId, changeAddress, invalidBefore, invalidHereafter, inputs } = request; + return unsafeTaskEither( + Transactions.postViaAxios(axiosInstance)( + contractId, + { + invalidBefore, + invalidHereafter, + version: request.version ?? "v1", metadata: request.metadata ?? {}, tags: request.tags ?? {}, - minUTxODeposit, - roles: request.roles, - threadRoleName: request.threadRoleName, - }; - const addressesAndCollaterals = { - changeAddress: request.changeAddress, + inputs, + }, + { + changeAddress, usedAddresses: request.usedAddresses ?? [], collateralUTxOs: request.collateralUTxOs ?? [], - }; - return unsafeTaskEither( - Contracts.postViaAxios(axiosInstance)(postContractsRequest, addressesAndCollaterals, request.stakeAddress) - ); - } + } + ) ); - }, - createContractSources(request) { - return withDynamicTypeCheck( - request, - (x) => Sources.CreateContractSourcesRequestGuard.decode(x), - strict, - (request) => { - const { - bundle: { main, bundle }, - } = request; - return Sources.createContractSources(axiosInstance)(main, bundle); - } + }), + submitWithdrawal: withDynamicTypeCheck(strict, Withdrawal.SubmitWithdrawalRequestGuard, (request) => { + const { withdrawalId, hexTransactionWitnessSet } = request; + return unsafeTaskEither(Withdrawal.putViaAxios(axiosInstance)(withdrawalId, hexTransactionWitnessSet)); + }), + getPayouts: withDynamicTypeCheck(strict, Payouts.GetPayoutsRequestGuard, async (request) => { + const { contractIds, roleTokens, range, status } = request; + return await unsafeTaskEither( + Payouts.getHeadersByRangeViaAxios(axiosInstance)(range)(contractIds)(roleTokens)(O.fromNullable(status)) ); - }, - getContractSourceById(request) { - return withDynamicTypeCheck( - request, - (x) => Sources.GetContractBySourceIdRequestGuard.decode(x), - strict, - (request) => { - return Sources.getContractSourceById(axiosInstance)(request); - } - ); - }, - getContractSourceAdjacency(request) { - return withDynamicTypeCheck( - request, - (x) => Sources.GetContractSourceAdjacencyRequestGuard.decode(x), - strict, - (request) => { - return Sources.getContractSourceAdjacency(axiosInstance)(request); - } - ); - }, - getContractSourceClosure(request) { - return withDynamicTypeCheck( - request, - (x) => Sources.GetContractSourceClosureRequestGuard.decode(x), - strict, - (request) => { - return Sources.getContractSourceClosure(axiosInstance)(request); - } - ); - }, - getNextStepsForContract(request) { - return withDynamicTypeCheck( - request, - (x) => Next.GetNextStepsForContractRequestGuard.decode(x), - strict, - (request) => { - return Next.getNextStepsForContract(axiosInstance)(request); - } - ); - }, - submitContract(request) { - return withDynamicTypeCheck( - request, - (x) => Contract.SubmitContractRequestGuard.decode(x), - strict, - (request) => { - const { contractId, txEnvelope } = request; - return Contract.submitContract(axiosInstance)(contractId, txEnvelope); - } - ); - }, - getTransactionsForContract(request) { - return withDynamicTypeCheck( - request, - (x) => Transactions.GetTransactionsForContractRequestGuard.decode(x), - strict, - (request) => { - const { contractId, range } = request; - return unsafeTaskEither(Transactions.getHeadersByRangeViaAxios(axiosInstance)(contractId, range)); - } - ); - }, - submitContractTransaction(request) { - return withDynamicTypeCheck( - request, - (x) => Transaction.SubmitContractTransactionRequestGuard.decode(x), - strict, - (request) => { - const { contractId, transactionId, hexTransactionWitnessSet } = request; - return unsafeTaskEither( - Transaction.putViaAxios(axiosInstance)(contractId, transactionId, hexTransactionWitnessSet) - ); - } - ); - }, - getContractTransactionById(request) { - return withDynamicTypeCheck( - request, - (x) => Transaction.GetContractTransactionByIdRequestGuard.decode(x), - strict, - (request) => { - const { contractId, txId } = request; - return unsafeTaskEither(Transaction.getViaAxios(axiosInstance)(contractId, txId)); - } - ); - }, - withdrawPayouts(request) { - return withDynamicTypeCheck( - request, - (x) => Withdrawals.WithdrawPayoutsRequestGuard.decode(x), - strict, - (request) => { - const { payoutIds, changeAddress } = request; - return unsafeTaskEither( - Withdrawals.postViaAxios(axiosInstance)(payoutIds, { - changeAddress, - usedAddresses: request.usedAddresses ?? [], - collateralUTxOs: request.collateralUTxOs ?? [], - }) - ); - } - ); - }, - async getWithdrawalById(request) { - return withDynamicTypeCheck( - request, - (x) => Withdrawal.GetWithdrawalByIdRequestGuard.decode(x), - strict, - async (request) => { - const { withdrawalId } = request; - const { block, ...response } = await unsafeTaskEither(Withdrawal.getViaAxios(axiosInstance)(withdrawalId)); - return { ...response, block: O.toUndefined(block) }; - } - ); - }, - getWithdrawals(request) { - return withDynamicTypeCheck( - request, - (x) => Withdrawals.GetWithdrawalsRequestGuard.decode(x), - strict, - (request) => { - return unsafeTaskEither(Withdrawals.getHeadersByRangeViaAxios(axiosInstance)(request)); - } - ); - }, - applyInputsToContract(request) { - return withDynamicTypeCheck( - request, - (x) => Transactions.ApplyInputsToContractRequestGuard.decode(x), - strict, - (request) => { - const { contractId, changeAddress, invalidBefore, invalidHereafter, inputs } = request; - return unsafeTaskEither( - Transactions.postViaAxios(axiosInstance)( - contractId, - { - invalidBefore, - invalidHereafter, - version: request.version ?? "v1", - metadata: request.metadata ?? {}, - tags: request.tags ?? {}, - inputs, - }, - { - changeAddress, - usedAddresses: request.usedAddresses ?? [], - collateralUTxOs: request.collateralUTxOs ?? [], - } - ) - ); - } - ); - }, - submitWithdrawal(request) { - return withDynamicTypeCheck( - request, - (x) => Withdrawal.SubmitWithdrawalRequestGuard.decode(x), - strict, - (request) => { - const { withdrawalId, hexTransactionWitnessSet } = request; - return unsafeTaskEither(Withdrawal.putViaAxios(axiosInstance)(withdrawalId, hexTransactionWitnessSet)); - } - ); - }, - async getPayouts(request) { - return withDynamicTypeCheck( - request, - (x) => Payouts.GetPayoutsRequestGuard.decode(x), - strict, - async (request) => { - const { contractIds, roleTokens, range, status } = request; - return await unsafeTaskEither( - Payouts.getHeadersByRangeViaAxios(axiosInstance)(range)(contractIds)(roleTokens)(O.fromNullable(status)) - ); - } - ); - }, - async getPayoutById(request) { - return withDynamicTypeCheck( - request, - (x) => Payout.GetPayoutByIdRequestGuard.decode(x), - strict, - async (request) => { - const { payoutId } = request; - const result = await unsafeTaskEither(Payout.getViaAxios(axiosInstance)(payoutId)); - return { - payoutId: result.payoutId, - contractId: result.contractId, - ...O.match( - () => ({}), - (withdrawalId) => ({ withdrawalId }) - )(result.withdrawalId), - role: result.role, - payoutValidatorAddress: result.payoutValidatorAddress, - status: result.status, - assets: result.assets, - }; - } - ); - }, + }), + getPayoutById: withDynamicTypeCheck(strict, Payout.GetPayoutByIdRequestGuard, async (request) => { + const { payoutId } = request; + const result = await unsafeTaskEither(Payout.getViaAxios(axiosInstance)(payoutId)); + return { + payoutId: result.payoutId, + contractId: result.contractId, + ...O.match( + () => ({}), + (withdrawalId) => ({ withdrawalId }) + )(result.withdrawalId), + role: result.role, + payoutValidatorAddress: result.payoutValidatorAddress, + status: result.status, + assets: result.assets, + }; + }), }; } @@ -637,7 +513,14 @@ export interface ContractsAPI { * @description Dependency Injection for the Rest Client API * @hidden */ -export type RestDI = { deprecatedRestAPI: FPTSRestAPI; restClient: RestClient }; +export type RestDI = { restClient: RestClient }; + +/** + * + * @description Dependency Injection for the Wallet API + * @hidden + */ +export type DeprecatedRestDI = { deprecatedRestAPI: FPTSRestAPI }; /** * @hidden diff --git a/packages/runtime/client/rest/src/runtime/version.ts b/packages/runtime/client/rest/src/runtime/version.ts index 6b1cec4c..ccf4e186 100644 --- a/packages/runtime/client/rest/src/runtime/version.ts +++ b/packages/runtime/client/rest/src/runtime/version.ts @@ -16,9 +16,10 @@ export type RuntimeVersion = t.TypeOf; export const runtimeVersion = (s: string) => unsafeEither(RuntimeVersionGuard.decode(s)); -export type CompatibleRuntimeVersion = "0.0.6" | "0.0.5"; +export type CompatibleRuntimeVersion = "1.0.0" | "0.0.6" | "0.0.5"; export const CompatibleRuntimeVersionGuard: t.Type = t.union([ + t.literal("1.0.0"), t.literal("0.0.6"), t.literal("0.0.5"), ]); diff --git a/packages/runtime/core/src/asset/index.ts b/packages/runtime/core/src/asset/index.ts index 1d224f6b..5306c0a1 100644 --- a/packages/runtime/core/src/asset/index.ts +++ b/packages/runtime/core/src/asset/index.ts @@ -3,6 +3,7 @@ import { PolicyId, PolicyIdGuard, policyId } from "../policyId.js"; import * as Marlowe from "@marlowe.io/language-core-v1"; import * as G from "@marlowe.io/language-core-v1/guards"; +import { assertGuardEqual, proxy, convertNullableToUndefined } from "@marlowe.io/adapter/io-ts"; export type AssetName = t.TypeOf; export const AssetName = t.string; @@ -37,11 +38,25 @@ export const token = export const lovelaces = (quantity: AssetQuantity): Token => token(quantity)(assetId(policyId(""))("")); -export type Tokens = t.TypeOf; -export const Tokens = t.array(Token); - -export type Assets = t.TypeOf; -export const Assets = t.type({ lovelaces: AssetQuantityGuard, tokens: Tokens }); +export type Tokens = Token[]; +export const TokensGuard = assertGuardEqual(proxy(), t.array(Token)); + +export type Assets = { + lovelaces?: AssetQuantity; + tokens: Tokens; +}; + +export const AssetsGuard = assertGuardEqual( + proxy(), + t.intersection([ + t.type({ + tokens: TokensGuard, + }), + t.partial({ + lovelaces: convertNullableToUndefined(AssetQuantityGuard), + }), + ]) +); export const assetIdToString: (assetId: AssetId) => string = (assetId) => `${assetId.policyId}|${assetId.assetName}`; @@ -67,6 +82,23 @@ export const unMapAsset: (assets: AssetsMap) => Assets = (restAssets) => ({ tokens: unMapTokens(restAssets.tokens), }); +export const mapAsset: (assets: Assets) => AssetsMap = (assets) => ({ + lovelace: assets.lovelaces ?? 0n, + tokens: mapTokens(assets.tokens), +}); + +export const mapTokens: (tokens: Tokens) => TokensMap = (tokens) => + tokens.reduce((acc, token) => { + const policyId = token.assetId.policyId; + const assetName = token.assetId.assetName; + const quantity = token.quantity; + if (acc[policyId] === undefined) { + acc[policyId] = {}; + } + acc[policyId][assetName] = quantity; + return acc; + }, {} as TokensMap); + export const unMapTokens: (tokens: TokensMap) => Tokens = (restTokens) => Object.entries(restTokens) .map(([aPolicyId, x]) => diff --git a/packages/runtime/core/src/contract/accountDeposits.ts b/packages/runtime/core/src/contract/accountDeposits.ts new file mode 100644 index 00000000..fa5b37bb --- /dev/null +++ b/packages/runtime/core/src/contract/accountDeposits.ts @@ -0,0 +1,24 @@ +import * as t from "io-ts/lib/index.js"; +import { Party, partyToString } from "@marlowe.io/language-core-v1"; +import { Assets, AssetsMap, AssetsMapGuard, mapAsset } from "../asset/index.js"; + +export type AddressOrRole = string; +export const AddressOrRoleGuard = t.string; +/** + * A map of tags to their content. The key is a string, the value can be anything. + * New feature from runtime v1.0.0 (initial account deposits)) + */ +export type AccountDeposits = { [key in AddressOrRole]: AssetsMap }; +/** + * @hidden + */ +export const AccountDepositsGuard = t.record(AddressOrRoleGuard, AssetsMapGuard); +/** + * a function that takes a list of party with associated assets and returns an accountDeposits + * the party object is converted to a string using partyToString. + * @param accounts + * @returns + */ +export function mkaccountDeposits(accounts: [Party, Assets][]): AccountDeposits { + return Object.fromEntries(accounts.map(([party, assets]) => [partyToString(party), mapAsset(assets)])); +} diff --git a/packages/runtime/core/src/index.ts b/packages/runtime/core/src/index.ts index 60eb9e60..bf861c2b 100644 --- a/packages/runtime/core/src/index.ts +++ b/packages/runtime/core/src/index.ts @@ -6,6 +6,7 @@ export * from "./tx/index.js"; export * from "./metadata.js"; export * from "./tag.js"; export * from "./contract/id.js"; +export * from "./contract/accountDeposits.js"; export * from "./sourceId.js"; export * from "./asset/index.js"; export * from "./payout/index.js"; diff --git a/packages/runtime/core/src/payout/index.ts b/packages/runtime/core/src/payout/index.ts index 11507002..15135fc9 100644 --- a/packages/runtime/core/src/payout/index.ts +++ b/packages/runtime/core/src/payout/index.ts @@ -6,7 +6,7 @@ import { pipe } from "fp-ts/lib/function.js"; import { head } from "fp-ts/lib/ReadonlyNonEmptyArray.js"; import { txId, TxId } from "../tx/id.js"; import { ContractIdGuard } from "../contract/id.js"; -import { AssetId, Assets } from "../asset/index.js"; +import { AssetId, Assets, AssetsGuard } from "../asset/index.js"; // QUESTION: @N.H: What is the difference between PayoutId and WithdrawalId? export type PayoutId = Newtype<{ readonly ContractId: unique symbol }, string>; @@ -31,7 +31,7 @@ export const PayoutAvailable = t.type({ payoutId: PayoutId, contractId: ContractIdGuard, role: AssetId, - assets: Assets, + assets: AssetsGuard, }); export type PayoutWithdrawn = t.TypeOf; @@ -40,5 +40,5 @@ export const PayoutWithdrawn = t.type({ payoutId: PayoutId, contractId: ContractIdGuard, role: AssetId, - assets: Assets, + assets: AssetsGuard, }); diff --git a/packages/runtime/lifecycle/src/api.ts b/packages/runtime/lifecycle/src/api.ts index a6097e9c..7a24d89f 100644 --- a/packages/runtime/lifecycle/src/api.ts +++ b/packages/runtime/lifecycle/src/api.ts @@ -1,22 +1,7 @@ import { WalletAPI, WalletDI } from "@marlowe.io/wallet/api"; -import { - AssetId, - ContractId, - Metadata, - PayoutAvailable, - PayoutId, - PayoutWithdrawn, - StakeAddressBech32, - Tags, - TxId, -} from "@marlowe.io/runtime-core"; -import { RestClient, RestDI } from "@marlowe.io/runtime-rest-client"; -import { RolesConfiguration } from "@marlowe.io/runtime-rest-client/contract"; -import { ISO8601 } from "@marlowe.io/adapter/time"; -import { Contract, Environment, Input, RoleName } from "@marlowe.io/language-core-v1"; -import { Next } from "@marlowe.io/language-core-v1/next"; -import { SingleInputTx } from "@marlowe.io/language-core-v1/transaction.js"; -import { ContractBundleList } from "@marlowe.io/marlowe-object"; +import { AssetId, ContractId, PayoutAvailable, PayoutId, PayoutWithdrawn } from "@marlowe.io/runtime-core"; +import { DeprecatedRestDI, RestClient, RestDI } from "@marlowe.io/runtime-rest-client"; + import { ApplicableActionsAPI, ApplicableAction, @@ -28,11 +13,23 @@ import { CanChoose, CanDeposit, CanNotify, - GetApplicableActionsResponse, - ActiveContract, - ClosedContract, - ContractDetails, } from "./generic/applicable-actions.js"; +import { ActiveContract, ClosedContract, ContractDetails, ContractInstanceAPI } from "./generic/new-contract-api.js"; +import { + ApplyInputsRequest, + ContractsAPI, + CreateContractRequest, + CreateContractRequestBase, + CreateContractRequestFromBundle, + CreateContractRequestFromContract, +} from "./generic/contracts.js"; +import { + ContractsAPI as NewContractsAPI, + ApplicableActionsAPI as NewApplicableActionsAPI, + EvaluateApplicableActionsRequest, +} from "./generic/new-contract-api.js"; +import * as NewContract from "./generic/new-contract-api.js"; + export { ApplicableActionsAPI, ApplicableAction, @@ -44,10 +41,19 @@ export { CanChoose, CanDeposit, CanNotify, - GetApplicableActionsResponse, ActiveContract, ClosedContract, ContractDetails, + ContractInstanceAPI, + CreateContractRequestBase, + ContractsAPI, + ApplyInputsRequest, + CreateContractRequest, + NewContractsAPI, + NewApplicableActionsAPI, + EvaluateApplicableActionsRequest as ComputeApplicableActionsRequest, + CreateContractRequestFromContract, + CreateContractRequestFromBundle, }; /** @@ -67,278 +73,25 @@ export interface RuntimeLifecycle { * Access to the low-level REST API as defined in the {@link @marlowe.io/runtime-rest-client! } package. It is re-exported here for convenience. */ restClient: RestClient; - /** - * The contracts API is a high level API that lets you create and interact with Marlowe contracts. - */ - contracts: ContractsAPI; - payouts: PayoutsAPI; - applicableActions: ApplicableActionsAPI; -} - -/** - * - * @description Dependency Injection for the Contract API - * @hidden - */ -export type ContractsDI = WalletDI & RestDI; -/** - * Request parameters used by {@link api.ContractsAPI#createContract | createContract}. - * If the contract is "small", you can create it directly with a {@link CreateContractRequestFromContract| core contract}, - * if the contract is "large" you can use a {@link CreateContractRequestFromBundle | contract bundle} instead. - * Both options share the same {@link CreateContractRequestBase | request parameters}. - * @category ContractsAPI - */ -export type CreateContractRequest = CreateContractRequestFromContract | CreateContractRequestFromBundle; - -/** - * @category ContractsAPI - */ -export interface CreateContractRequestFromContract extends CreateContractRequestBase { /** - * The Marlowe Contract to create + * The new contract API is a high level API that lets you create and interact with Marlowe contracts. */ - contract: Contract; -} + newContractAPI: NewContract.ContractsAPI; -/** - * @category ContractsAPI - */ -export interface CreateContractRequestFromBundle extends CreateContractRequestBase { /** - * The Marlowe Object bundle to create - */ - bundle: ContractBundleList; -} - -/** - * @category ContractsAPI - */ -export interface CreateContractRequestBase { - /** - * Marlowe contracts can have staking rewards for the ADA locked in the contract. - * Use this field to set the recipient address of those rewards - */ - stakeAddress?: StakeAddressBech32; - /** - * @experimental - * The Thread Roles capability is an implementation details of the runtime. - * It allows you to provide a custom name if the thread role name is conflicting with other role names used. - * @default - * - the Thread Role Name is "" by default. - * @see - * - https://github.com/input-output-hk/marlowe-cardano/blob/main/marlowe-runtime/doc/open-roles.md - */ - threadRoleName?: RoleName; - - /** - * Role Token Configuration for the new contract passed in the `contract` field. - * - *

Participants

- *

- * Participants ({@link @marlowe.io/language-core-v1!index.Party | Party}) in a Marlowe Contract can be expressed in 2 ways: - * - * 1. **By Adressses** : When an address is fixed in the contract we don't need to provide further configuration. - * 2. **By Roles** : When the participation is done through a Role Token, we need to define if that token is minted as part of the contract creation transaction or if it was minted beforehand. - * - *

- * - *

Configuration Options

- *

- * - * - **When to create (mint)** - * - **Within the Runtime** : At the contrat creation, these defined Roles Tokens will be minted "on the fly" by the runtime. - * - **Without the Runtime** : before the creation, these Role Tokens are already defined (via an NFT platform, `cardano-cli`, another Marlowe Contract Created, etc.. ) - * - **How to distribute** - * - **Closedly** (Closed Roles) : At the creation of contract or before, the Role Tokens are released to the participants. All the participants are known at the creation and therefore we consider the participation as being closed. - * - **Openly** (Open Roles) : Whoever applies an input (IDeposit or IChoice) on the contract `contract` first will be identified as a participant by receiving the Role Token in their wallet. In that case, participants are unknown at the creation and the participation is open to any meeting the criteria. - * - **With or without Metadata** - * - **Quantities to create(Mint)** : When asking to mint the tokens within the Runtime, quantities can defined as well. - * - * Smart Constructors are available to ease these configuration: - * - {@link @marlowe.io/runtime-rest-client!contract.useMintedRoles} - * - {@link @marlowe.io/runtime-rest-client!contract.mintRole} - * - * @remarks - * - The Distribution can be a mix of Closed and Open Role Tokens configuration. See examples below. - *

- * - * @example - * - * ```ts - * ////////////// - * // #1 - Mint Role Tokens - * ////////////// - * const anAddressBech32 = "addr_test1qqe342swyfn75mp2anj45f8ythjyxg6m7pu0pznptl6f2d84kwuzrh8c83gzhrq5zcw7ytmqc863z5rhhwst3w4x87eq0td9ja" - * const aMintingConfiguration = - * { "closed_Role_A_NFT" : mintRole(anAddressBech32) - * , "closed_Role_B_FT" : - * mintRole( - * anAddressBech32, - * 5, // Quantities - * { "name": "closed_Role_B_FT Marlowe Role Token", - * "description": "These are metadata for closedRoleB", - * image": "ipfs://QmaQMH7ybS9KmdYQpa4FMtAhwJH5cNaacpg4fTwhfPvcwj", - * "mediaType": "image/png", - * "files": [ - * { - * "name": "icon-1000", - * "mediaType": "image/webp", - * "src": "ipfs://QmUbvavFxGSSEo3ipQf7rjrELDvXHDshWkHZSpV8CVdSE5" - * } - * ] - * }) - * , "open_Role_C" : mkMintOpenRoleToken() - * , "open_Role_D" : mkMintOpenRoleToken( - * 2, // Quantities - * { "name": "open_Role_D Marlowe Role Token", - * "description": "These are metadata for closedRoleB", - * image": "ipfs://QmaQMH7ybS9KmdYQpa4FMtAhwJH5cNaacpg4fTwhfPvcwj", - * "mediaType": "image/png", - * "files": [ - * { - * "name": "icon-1000", - * "mediaType": "image/webp", - * "src": "ipfs://QmUbvavFxGSSEo3ipQf7rjrELDvXHDshWkHZSpV8CVdSE5" - * } - * ] - * }) - * } - * - * ////////////// - * // #2 Use Minted Roles Tokens - * const aUseMintedRoleTokensConfiguration = - * useMintedRoles( - * "e68f1cea19752d1292b4be71b7f5d2b3219a15859c028f7454f66cdf", - * ["role_A","role_C"] - * ) - * ``` - * - * @see - * - {@link @marlowe.io/runtime-rest-client!contract.useMintedRoles} - * - {@link @marlowe.io/runtime-rest-client!contract.mintRole} - * - Open Roles Runtime Implementation : https://github.com/input-output-hk/marlowe-cardano/blob/main/marlowe-runtime/doc/open-roles.md - */ - roles?: RolesConfiguration; - - /** - * Marlowe Tags are stored as Metadata within the Transaction Metadata under the top-level Marlowe Reserved Key (`1564`). - * Tags allows to Query created Marlowe Contracts via {@link @marlowe.io/runtime-rest-client!index.RestClient#getContracts | Get contracts } - * - *

Properties

- * - * 1. They aren't limited size-wise like regular metadata fields are over Cardano. - * 2. Metadata can be associated under each tag - * - * @example - * ```ts - * const myTags : Tags = { "My Tag 1 That can be as long as I want": // Not limited to 64 bytes - * { a: 0 - * , b : "Tag 1 content" // Limited to 64 bytes (Cardano Metadata constraint) - * }, - * "MyTag2": { c: 0, d : "Tag 2 content"}}; - * ``` - */ - tags?: Tags; - /** - * Cardano Metadata about the contract creation. - *

Properties

- *

- * Metadata can be expressed as a JSON object with some restrictions: - * - All top-level keys must be integers between 0 and 2^63 - 1. - * - Each metadata value is tagged with its type. - * - Strings must be at most 64 characters long (64 bytes) when UTF-8 is encoded. - * - Bytestrings are hex-encoded, with a maximum length of 64 bytes. - * - * Metadata aren't stored as JSON on the Cardano blockchain but are instead stored using a compact binary encoding (CBOR). - * The binary encoding of metadata values supports three simple types: - * - Integers in the range `-(2^63 - 1)` to `2^63 - 1` - * - Strings (UTF-8 encoded) - * - Bytestrings - * - And two compound types: - * - Lists of metadata values - * - Mappings from metadata values to metadata values - *

- * It is possible to transform any JSON object into this schema (See https://developers.cardano.org/docs/transaction-metadata ) - * @see - * https://developers.cardano.org/docs/transaction-metadata - */ - metadata?: Metadata; - - /** - * Minimum Lovelace value to add on the UTxO created (Representing the Marlowe Contract Created on the ledger).This value - * is computed automatically within the Runtime, so this parameter is only necessary if you need some custom adjustment. - * - *

Justification

- *

Creating a Marlowe contract over Cardano is about creating UTxO entries on the Ledger. - * - * To protect the ledger from growing beyond a certain size that will cost too much to maintain, - * a constraint called "Minimum ada value requirement (minimumLovelaceUTxODeposit)" that adjust - * the value (in ADA) of each UTxO has been added. - * - * The bigger the UTxOs entries are in terms of bytesize, the higher the value if minimum ADA required.

- * - * @see - * https://docs.cardano.org/native-tokens/minimum-ada-value-requirement - */ - minimumLovelaceUTxODeposit?: number; -} - -/** - * @category ContractsAPI - */ -export type ApplyInputsRequest = { - inputs: Input[]; - tags?: Tags; - metadata?: Metadata; - invalidBefore?: ISO8601; - invalidHereafter?: ISO8601; -}; - -/** - * This Interface provides capabilities for runnning a Contract over Cardano. - * @category ContractsAPI - */ -export interface ContractsAPI { - /** - * Submit to the Cardano Ledger, the Transaction(Tx) that will create the Marlowe Contract passed in the request. - * @param createContractRequest Request parameters for creating a Marlowe Contract on Cardano - * @returns ContractId (Marlowe id) and TxId (Cardano id) of the submitted Tx - * @throws DecodingError - */ - createContract(createContractRequest: CreateContractRequest): Promise<[ContractId, TxId]>; - - /** - * Submit to the Cardano Ledger, the Transaction(Tx) that will apply inputs to a given created contract. - * @param contractId Contract Id where inputs will be applied - * @param applyInputsRequest inputs to apply - * @throws DecodingError - */ - applyInputs(contractId: ContractId, applyInputsRequest: ApplyInputsRequest): Promise; - - /** - * @deprecated Deprecated in favour of {@link @marlowe.io/runtime-lifecycle!api.ApplicableActionsAPI} - */ - getApplicableInputs(contractId: ContractId, environement: Environment): Promise; - - /** - * @description - * Fetches all contract ids for contracts on chain that mentions an address in your wallet. - * @throws Error | DecodingError - */ - getContractIds(): Promise; - - /** - * Get a list of the applied inputs for a given contract - * @param contractId + * The contracts API is a high level API that lets you create and interact with Marlowe contracts. + * @deprecated Use {@link RuntimeLifecycle.newContractAPI} instead. */ - getInputHistory(contractId: ContractId): Promise; + contracts: ContractsAPI; + payouts: PayoutsAPI; + applicableActions: ApplicableActionsAPI; } /** * @hidden */ -export type PayoutsDI = WalletDI & RestDI; +export type PayoutsDI = WalletDI & RestDI & DeprecatedRestDI; /** * @category PayoutsAPI diff --git a/packages/runtime/lifecycle/src/browser/index.ts b/packages/runtime/lifecycle/src/browser/index.ts index d343e928..5889e5c0 100644 --- a/packages/runtime/lifecycle/src/browser/index.ts +++ b/packages/runtime/lifecycle/src/browser/index.ts @@ -41,7 +41,7 @@ import * as Generic from "../generic/runtime.js"; import { mkFPTSRestClient, mkRestClient } from "@marlowe.io/runtime-rest-client"; import { RuntimeLifecycle } from "../api.js"; -import { InvalidTypeError, strictDynamicTypeCheck } from "@marlowe.io/adapter/io-ts"; +import { InvalidTypeError, dynamicAssertType } from "@marlowe.io/adapter/io-ts"; import * as t from "io-ts/lib/index.js"; /** @@ -69,25 +69,6 @@ export const BrowserRuntimeLifecycleOptionsGuard: t.Type; /** * Creates an instance of RuntimeLifecycle using the browser wallet. * @param options @@ -98,12 +79,8 @@ export async function mkRuntimeLifecycle( options: BrowserRuntimeLifecycleOptions, strict = true ): Promise { - if (!strictDynamicTypeCheck(strict)) { - throw new InvalidTypeError([], `Invalid type for argument 'strict', expected boolean but got ${strict}`); - } - if (!mkRuntimeLifecycleArgumentDynamicTypeCheck(options, strict)) { - throw new InvalidTypeError([], `Invalid type for argument 'options', expected string but got ${options}`); - } + dynamicAssertType(BrowserRuntimeLifecycleOptionsGuard, options); + dynamicAssertType(t.boolean, strict, "Invalid type for argument 'strict', expected boolean"); const { runtimeURL, walletName } = options; const wallet = await mkBrowserWallet(walletName); diff --git a/packages/runtime/lifecycle/src/generic/applicable-actions.ts b/packages/runtime/lifecycle/src/generic/applicable-actions.ts index 00aeb534..770931f7 100644 --- a/packages/runtime/lifecycle/src/generic/applicable-actions.ts +++ b/packages/runtime/lifecycle/src/generic/applicable-actions.ts @@ -1,4 +1,4 @@ -import { ContractsAPI } from "../api.js"; +import { ApplyInputsRequest, ContractsAPI, applyInputs } from "./contracts.js"; import { Monoid } from "fp-ts/lib/Monoid.js"; import * as R from "fp-ts/lib/Record.js"; @@ -33,11 +33,12 @@ import { TransactionSuccess, } from "@marlowe.io/language-core-v1/semantics"; import { AddressBech32, ContractId, Metadata, PolicyId, Tags, TxId } from "@marlowe.io/runtime-core"; -import { RestClient, Tip } from "@marlowe.io/runtime-rest-client"; -import { WalletAPI } from "@marlowe.io/wallet"; +import { RestDI } from "@marlowe.io/runtime-rest-client"; +import { WalletAPI, WalletDI } from "@marlowe.io/wallet"; import * as Big from "@marlowe.io/adapter/bigint"; import { ContractSourceId } from "@marlowe.io/marlowe-object"; import { posixTimeToIso8601 } from "@marlowe.io/adapter/time"; +import { ActiveContract, ContractDetails } from "./new-contract-api.js"; /** * @experimental @@ -51,13 +52,8 @@ export interface ApplicableActionsAPI { * the environment is computed using the runtime tip as a lower bound and the next timeout * as an upper bound. * @returns An object with an array of {@link ApplicableAction | applicable actions} and the {@link ContractDetails | contract details} - * @experimental - * @remarks - * **EXPERIMENTAL:** Perhaps instead of receiving a contractId and returning the actions and contractDetails this - * function should receive the contractDetails and just return the actions. - * To do this, we should refactor the {@link ContractsAPI} first to use the {@link ContractDetails} type */ - getApplicableActions(contractId: ContractId, environment?: Environment): Promise; + getApplicableActions(contractDetails: ContractDetails, environment?: Environment): Promise; /** * Converts an {@link ApplicableAction} into an {@link ApplicableInput}. @@ -77,7 +73,8 @@ export interface ApplicableActionsAPI { getInput(contractDetails: ActiveContract, action: CanChoose, chosenNum: ChosenNum): Promise; /** - * Applies an input to a contract. This is a wrapper around the {@link ContractsAPI.applyInputs | Contracts's API applyInputs} function. + * Applies an {@link ApplicableInput} to a contract. + * @returns The transaction id of the applied input. */ applyInput(contractId: ContractId, request: ApplyApplicableInputRequest): Promise; /** @@ -88,6 +85,12 @@ export interface ApplicableActionsAPI { */ simulateInput(contractDetails: ActiveContract, input: ApplicableInput): TransactionSuccess; + /** + * Computes the environment for a contract. The environment is computed using the runtime tip as a lower bound and the next timeout + * as an upper bound. + */ + computeEnvironment: (contract: Contract) => Promise; + /** * Creates a filter function for the {@link ApplicableAction | applicable actions} of the wallet owner. * The wallet is configured when we instantiate the {@link RuntimeLifecycle}. This function returns a new @@ -125,19 +128,13 @@ export interface ApplyApplicableInputRequest { /** * @hidden */ -export function mkApplicableActionsAPI( - restClient: RestClient, - wallet: WalletAPI, - contractDI: ContractsAPI -): ApplicableActionsAPI { - const di = mkGetApplicableActionsDI(restClient); - +export function mkApplicableActionsAPI(di: RestDI & WalletDI & GetContinuationDI & ChainTipDI): ApplicableActionsAPI { async function mkFilter(): Promise; async function mkFilter(contractDetails: ActiveContract): Promise; async function mkFilter( contractDetails?: ActiveContract ): Promise { - const curriedFilter = await mkApplicableActionsFilter(wallet); + const curriedFilter = await mkApplicableActionsFilter(di.wallet); if (contractDetails) { return (action: ApplicableAction) => curriedFilter(action, contractDetails); } else { @@ -149,14 +146,17 @@ export function mkApplicableActionsAPI( getInput: getApplicableInput(di), simulateInput: simulateApplicableInput, getApplicableActions: getApplicableActions(di), - applyInput: applyInput(contractDI), + computeEnvironment: (contract) => computeEnvironment(di, contract), + applyInput: applyInput(applyInputs(di)), mkFilter, }; } -function applyInput(contractDI: ContractsAPI) { +export type ApplyInputDI = (contractId: ContractId, request: ApplyInputsRequest) => Promise; + +function applyInput(doApply: ApplyInputDI) { return async function (contractId: ContractId, request: ApplyApplicableInputRequest): Promise { - return contractDI.applyInputs(contractId, { + return doApply(contractId, { inputs: request.input.inputs, tags: request.tags, metadata: request.metadata, @@ -165,7 +165,7 @@ function applyInput(contractDI: ContractsAPI) { // way into the future and the time to slot conversion is undefined if the // end time passes a certain threshold. // We currently don't have the network parameters to do the conversion ourselves - // so we leave to the runtime to calculate an adecuate max slot. + // so we leave to the runtime to calculate an adequate max slot. // This might cause some issues if the contract relies on the TimeIntervalEnd value // as the result of simulating and applying the input might differ. // invalidHereafter: posixTimeToIso8601( @@ -174,13 +174,6 @@ function applyInput(contractDI: ContractsAPI) { }); }; } -/** - * @category ApplicableActionsAPI - */ -export interface GetApplicableActionsResponse { - actions: ApplicableAction[]; - contractDetails: ContractDetails; -} type ActionApplicant = Party | "anybody"; @@ -207,7 +200,7 @@ export interface CanNotify { /** * Discriminator field, used to differentiate the action type. */ - actionType: "Notify"; + type: "Notify"; /** * If the When's case is merkleized, this is the hash of the merkleized continuation. */ @@ -226,7 +219,7 @@ export interface CanDeposit { /** * Discriminator field, used to differentiate the action type. */ - actionType: "Deposit"; + type: "Deposit"; /** * If the When's case is merkleized, this is the hash of the merkleized continuation. */ @@ -249,7 +242,7 @@ export interface CanChoose { /** * Discriminator field, used to differentiate the action type. */ - actionType: "Choice"; + type: "Choice"; /** * If the When's case is merkleized, this is the hash of the merkleized continuation. */ @@ -272,7 +265,7 @@ export interface CanChoose { * @category ApplicableActionsAPI */ export interface CanAdvance { - actionType: "Advance"; + type: "Advance"; environment: Environment; } @@ -331,7 +324,7 @@ export function getApplicableInput(di: GetContinuationDI) { } const environment = action.environment; - switch (action.actionType) { + switch (action.type) { case "Advance": return { inputs: [], @@ -409,7 +402,7 @@ export function simulateApplicableInput( } function getApplicant(action: ApplicableAction): ActionApplicant { - switch (action.actionType) { + switch (action.type) { case "Notify": case "Advance": return "anybody"; @@ -420,7 +413,10 @@ function getApplicant(action: ApplicableAction): ActionApplicant { } } -type ChainTipDI = { +/** + * @hidden + */ +export type ChainTipDI = { getRuntimeTip: () => Promise; }; @@ -449,46 +445,15 @@ async function computeEnvironment({ getRuntimeTip }: ChainTipDI, currentContract return { timeInterval: { from: lowerBound, to: upperBound - 1n } }; } -/** - * @hidden - */ -export const mkGetApplicableActionsDI = (restClient: RestClient): GetApplicableActionsDI => { - return { - getContractContinuation: (contractSourceId: ContractSourceId) => { - // TODO: Add caching - return restClient.getContractSourceById({ contractSourceId }); - }, - getContractDetails: async (contractId: ContractId) => { - const contractDetails = await restClient.getContractById({ contractId }); - if (typeof contractDetails.state === "undefined" || typeof contractDetails.currentContract === "undefined") { - return { type: "closed" }; - } else { - return { - type: "active", - contractId, - currentState: contractDetails.state, - currentContract: contractDetails.currentContract, - roleTokenMintingPolicyId: contractDetails.roleTokenMintingPolicyId, - }; - } - }, - getRuntimeTip: async () => { - const status = await restClient.healthcheck(); - return new Date(status.tips.runtimeChain.slotTimeUTC); - }, - }; -}; - -type GetApplicableActionsDI = GetContinuationDI & GetContractDetailsDI & ChainTipDI; +type GetApplicableActionsDI = GetContinuationDI & ChainTipDI; /** * @hidden */ export function getApplicableActions(di: GetApplicableActionsDI) { - return async function (contractId: ContractId, environment?: Environment): Promise { - const contractDetails = await di.getContractDetails(contractId); + return async function (contractDetails: ContractDetails, environment?: Environment): Promise { // If the contract is closed there are no applicable actions - if (contractDetails.type === "closed") return { contractDetails, actions: [] }; + if (contractDetails.type === "closed") return []; const env = environment ?? (await computeEnvironment(di, contractDetails.currentContract)); @@ -502,7 +467,7 @@ export function getApplicableActions(di: GetApplicableActionsDI) { let applicableActions: ApplicableAction[] = initialReduce.reduced ? [ { - actionType: "Advance", + type: "Advance", environment: env, }, ] @@ -521,10 +486,7 @@ export function getApplicableActions(di: GetApplicableActionsDI) { ) ); } - return { - contractDetails, - actions: applicableActions, - }; + return applicableActions; }; } @@ -698,7 +660,7 @@ const flattenChoices = { concat: (fst: CanChoose, snd: CanChoose): CanChoose => { const mergedBounds = mergeBounds(fst.choice.choose_between.concat(snd.choice.choose_between)); return { - actionType: "Choice", + type: "Choice", environment: fst.environment, choice: { for_choice: fst.choice.for_choice, @@ -723,7 +685,10 @@ const mergeApplicableActionAccumulator: Monoid = { }, }; -type GetContinuationDI = { +/** + * @hidden + */ +export type GetContinuationDI = { getContractContinuation: (sourceId: ContractSourceId) => Promise; }; @@ -731,7 +696,7 @@ function getApplicableActionFromCase(env: Environment, state: MarloweState, aCas if (isDepositAction(aCase.case)) { const deposit = aCase.case; return accumulatorFromDeposit(env, state, { - actionType: "Deposit", + type: "Deposit", deposit, environment: env, }); @@ -739,7 +704,7 @@ function getApplicableActionFromCase(env: Environment, state: MarloweState, aCas const choice = aCase.case; return accumulatorFromChoice({ - actionType: "Choice", + type: "Choice", choice, environment: env, }); @@ -750,42 +715,8 @@ function getApplicableActionFromCase(env: Environment, state: MarloweState, aCas } return accumulatorFromNotify({ - actionType: "Notify", + type: "Notify", environment: env, }); } } - -// #region High level Contract Details -/** - * @category New ContractsAPI - */ -export type ClosedContract = { - type: "closed"; -}; - -/** - * @category New ContractsAPI - */ -export type ActiveContract = { - type: "active"; - contractId: ContractId; - currentState: MarloweState; - currentContract: Contract; - roleTokenMintingPolicyId: PolicyId; -}; - -/** - * This is the start of a high level API to get the contract details. - * The current restAPI is not clear wether the details that you get are - * from a closed or active contract. This API is just the start to get - * getApplicableInputs ready in production, but as part of a ContractsAPI - * refactoring, the whole contract details should be modeled. - * @category New ContractsAPI - */ -export type ContractDetails = ClosedContract | ActiveContract; - -type GetContractDetailsDI = { - getContractDetails: (contractId: ContractId) => Promise; -}; -// #endregion diff --git a/packages/runtime/lifecycle/src/generic/contracts.ts b/packages/runtime/lifecycle/src/generic/contracts.ts index 852bf3de..73969590 100644 --- a/packages/runtime/lifecycle/src/generic/contracts.ts +++ b/packages/runtime/lifecycle/src/generic/contracts.ts @@ -1,39 +1,306 @@ -import * as TE from "fp-ts/lib/TaskEither.js"; -import { pipe } from "fp-ts/lib/function.js"; import { Option } from "fp-ts/lib/Option.js"; -import { Environment, Party } from "@marlowe.io/language-core-v1"; -import { tryCatchDefault, unsafeTaskEither } from "@marlowe.io/adapter/fp-ts"; -import { - ApplyInputsRequest, - ContractsAPI, - ContractsDI, - CreateContractRequest, - CreateContractRequestBase, -} from "../api.js"; - -import { getAddressesAndCollaterals, WalletAPI } from "@marlowe.io/wallet/api"; +import { Contract, Environment, Input, Party, RoleName } from "@marlowe.io/language-core-v1"; +import { unsafeTaskEither } from "@marlowe.io/adapter/fp-ts"; + +import { getAddressesAndCollaterals, WalletAPI, WalletDI } from "@marlowe.io/wallet/api"; import { PolicyId, ContractId, contractIdToTxId, TxId, - AddressesAndCollaterals, - HexTransactionWitnessSet, transactionWitnessSetTextEnvelope, BlockHeader, + StakeAddressBech32, + Metadata, + Tags, + AccountDeposits, } from "@marlowe.io/runtime-core"; -import { FPTSRestAPI, RestClient, ItemRange } from "@marlowe.io/runtime-rest-client"; -import { DecodingError } from "@marlowe.io/adapter/codec"; +import { FPTSRestAPI, RestClient, RestDI, ItemRange, DeprecatedRestDI } from "@marlowe.io/runtime-rest-client"; import { Next, noNext } from "@marlowe.io/language-core-v1/next"; import { BuildCreateContractTxRequest, BuildCreateContractTxRequestOptions, - TransactionTextEnvelope, + RolesConfiguration, } from "@marlowe.io/runtime-rest-client/contract"; import { SingleInputTx } from "@marlowe.io/language-core-v1/transaction.js"; -import { iso8601ToPosixTime } from "@marlowe.io/adapter/time"; +import { ISO8601, iso8601ToPosixTime } from "@marlowe.io/adapter/time"; +import { ContractBundleList } from "@marlowe.io/marlowe-object"; + +/** + * + * @description Dependency Injection for the Contract API + * @hidden + */ +export type ContractsDI = WalletDI & RestDI; + +/** + * Request parameters used by {@link api.ContractsAPI#createContract | createContract}. + * If the contract is "small", you can create it directly with a {@link CreateContractRequestFromContract| core contract}, + * if the contract is "large" you can use a {@link CreateContractRequestFromBundle | contract bundle} instead. + * Both options share the same {@link CreateContractRequestBase | request parameters}. + * @category ContractsAPI + */ +export type CreateContractRequest = CreateContractRequestFromContract | CreateContractRequestFromBundle; + +/** + * @category ContractsAPI + */ +export interface CreateContractRequestFromContract extends CreateContractRequestBase { + /** + * The Marlowe Contract to create + */ + contract: Contract; +} + +/** + * @category ContractsAPI + */ +export interface CreateContractRequestFromBundle extends CreateContractRequestBase { + /** + * The Marlowe Object bundle to create + */ + bundle: ContractBundleList; +} + +/** + * @category ContractsAPI + */ +export interface CreateContractRequestBase { + /** + * Marlowe contracts can have staking rewards for the ADA locked in the contract. + * Use this field to set the recipient address of those rewards + */ + stakeAddress?: StakeAddressBech32; + /** + * @experimental + * The Thread Roles capability is an implementation details of the runtime. + * It allows you to provide a custom name if the thread role name is conflicting with other role names used. + * @default + * - the Thread Role Name is "" by default. + * @see + * - https://github.com/input-output-hk/marlowe-cardano/blob/main/marlowe-runtime/doc/open-roles.md + */ + threadRoleName?: RoleName; + + /** + * Initial Accounts State for the contract creation. + *

Properties

+ *

+ * The initial accounts state is a mapping of addresses or role tokens to their initial assets. + * The creator of the contract can define the initial assets for each participant. + * Assets will be withdrawn from the creator's wallet and deposited into the participant's accounts when the contract is created. + *

+ * @see + * - {@link @marlowe.io/runtime-core!index.AccountDeposits} + */ + + accountDeposits?: AccountDeposits; + /** + * Role Token Configuration for the new contract passed in the `contract` field. + * + *

Participants

+ *

+ * Participants ({@link @marlowe.io/language-core-v1!index.Party | Party}) in a Marlowe Contract can be expressed in 2 ways: + * + * 1. **By Adressses** : When an address is fixed in the contract we don't need to provide further configuration. + * 2. **By Roles** : When the participation is done through a Role Token, we need to define if that token is minted as part of the contract creation transaction or if it was minted beforehand. + * + *

+ * + *

Configuration Options

+ *

+ * + * - **When to create (mint)** + * - **Within the Runtime** : At the contrat creation, these defined Roles Tokens will be minted "on the fly" by the runtime. + * - **Without the Runtime** : before the creation, these Role Tokens are already defined (via an NFT platform, `cardano-cli`, another Marlowe Contract Created, etc.. ) + * - **How to distribute** + * - **Closedly** (Closed Roles) : At the creation of contract or before, the Role Tokens are released to the participants. All the participants are known at the creation and therefore we consider the participation as being closed. + * - **Openly** (Open Roles) : Whoever applies an input (IDeposit or IChoice) on the contract `contract` first will be identified as a participant by receiving the Role Token in their wallet. In that case, participants are unknown at the creation and the participation is open to any meeting the criteria. + * - **With or without Metadata** + * - **Quantities to create(Mint)** : When asking to mint the tokens within the Runtime, quantities can defined as well. + * + * Smart Constructors are available to ease these configuration: + * - {@link @marlowe.io/runtime-rest-client!contract.useMintedRoles} + * - {@link @marlowe.io/runtime-rest-client!contract.mintRole} + * + * @remarks + * - The Distribution can be a mix of Closed and Open Role Tokens configuration. See examples below. + *

+ * + * @example + * + * ```ts + * ////////////// + * // #1 - Mint Role Tokens + * ////////////// + * const anAddressBech32 = "addr_test1qqe342swyfn75mp2anj45f8ythjyxg6m7pu0pznptl6f2d84kwuzrh8c83gzhrq5zcw7ytmqc863z5rhhwst3w4x87eq0td9ja" + * const aMintingConfiguration = + * { "closed_Role_A_NFT" : mintRole(anAddressBech32) + * , "closed_Role_B_FT" : + * mintRole( + * anAddressBech32, + * 5, // Quantities + * { "name": "closed_Role_B_FT Marlowe Role Token", + * "description": "These are metadata for closedRoleB", + * image": "ipfs://QmaQMH7ybS9KmdYQpa4FMtAhwJH5cNaacpg4fTwhfPvcwj", + * "mediaType": "image/png", + * "files": [ + * { + * "name": "icon-1000", + * "mediaType": "image/webp", + * "src": "ipfs://QmUbvavFxGSSEo3ipQf7rjrELDvXHDshWkHZSpV8CVdSE5" + * } + * ] + * }) + * , "open_Role_C" : mkMintOpenRoleToken() + * , "open_Role_D" : mkMintOpenRoleToken( + * 2, // Quantities + * { "name": "open_Role_D Marlowe Role Token", + * "description": "These are metadata for closedRoleB", + * image": "ipfs://QmaQMH7ybS9KmdYQpa4FMtAhwJH5cNaacpg4fTwhfPvcwj", + * "mediaType": "image/png", + * "files": [ + * { + * "name": "icon-1000", + * "mediaType": "image/webp", + * "src": "ipfs://QmUbvavFxGSSEo3ipQf7rjrELDvXHDshWkHZSpV8CVdSE5" + * } + * ] + * }) + * } + * + * ////////////// + * // #2 Use Minted Roles Tokens + * const aUseMintedRoleTokensConfiguration = + * useMintedRoles( + * "e68f1cea19752d1292b4be71b7f5d2b3219a15859c028f7454f66cdf", + * ["role_A","role_C"] + * ) + * ``` + * + * @see + * - {@link @marlowe.io/runtime-rest-client!contract.useMintedRoles} + * - {@link @marlowe.io/runtime-rest-client!contract.mintRole} + * - Open Roles Runtime Implementation : https://github.com/input-output-hk/marlowe-cardano/blob/main/marlowe-runtime/doc/open-roles.md + */ + roles?: RolesConfiguration; + + /** + * Marlowe Tags are stored as Metadata within the Transaction Metadata under the top-level Marlowe Reserved Key (`1564`). + * Tags allows to Query created Marlowe Contracts via {@link @marlowe.io/runtime-rest-client!index.RestClient#getContracts | Get contracts } + * + *

Properties

+ * + * 1. They aren't limited size-wise like regular metadata fields are over Cardano. + * 2. Metadata can be associated under each tag + * + * @example + * ```ts + * const myTags : Tags = { "My Tag 1 That can be as long as I want": // Not limited to 64 bytes + * { a: 0 + * , b : "Tag 1 content" // Limited to 64 bytes (Cardano Metadata constraint) + * }, + * "MyTag2": { c: 0, d : "Tag 2 content"}}; + * ``` + */ + tags?: Tags; + /** + * Cardano Metadata about the contract creation. + *

Properties

+ *

+ * Metadata can be expressed as a JSON object with some restrictions: + * - All top-level keys must be integers between 0 and 2^63 - 1. + * - Each metadata value is tagged with its type. + * - Strings must be at most 64 characters long (64 bytes) when UTF-8 is encoded. + * - Bytestrings are hex-encoded, with a maximum length of 64 bytes. + * + * Metadata aren't stored as JSON on the Cardano blockchain but are instead stored using a compact binary encoding (CBOR). + * The binary encoding of metadata values supports three simple types: + * - Integers in the range `-(2^63 - 1)` to `2^63 - 1` + * - Strings (UTF-8 encoded) + * - Bytestrings + * - And two compound types: + * - Lists of metadata values + * - Mappings from metadata values to metadata values + *

+ * It is possible to transform any JSON object into this schema (See https://developers.cardano.org/docs/transaction-metadata ) + * @see + * https://developers.cardano.org/docs/transaction-metadata + */ + metadata?: Metadata; + + /** + * Minimum Lovelace value to add on the UTxO created (Representing the Marlowe Contract Created on the ledger).This value + * is computed automatically within the Runtime, so this parameter is only necessary if you need some custom adjustment. + * + *

Justification

+ *

Creating a Marlowe contract over Cardano is about creating UTxO entries on the Ledger. + * + * To protect the ledger from growing beyond a certain size that will cost too much to maintain, + * a constraint called "Minimum ada value requirement (minimumLovelaceUTxODeposit)" that adjust + * the value (in ADA) of each UTxO has been added. + * + * The bigger the UTxOs entries are in terms of bytesize, the higher the value if minimum ADA required.

+ * + * @see + * https://docs.cardano.org/native-tokens/minimum-ada-value-requirement + */ + minimumLovelaceUTxODeposit?: number; +} + +/** + * Request parameters used by {@link api.ContractsAPI#applyInputs | applyInputs}. + * @category ContractsAPI + */ +export type ApplyInputsRequest = { + inputs: Input[]; + tags?: Tags; + metadata?: Metadata; + invalidBefore?: ISO8601; + invalidHereafter?: ISO8601; +}; + +/** + * This Interface provides capabilities for runnning a Contract over Cardano. + * @category ContractsAPI + */ +export interface ContractsAPI { + /** + * Submit to the Cardano Ledger, the Transaction(Tx) that will create the Marlowe Contract passed in the request. + * @param createContractRequest Request parameters for creating a Marlowe Contract on Cardano + * @returns ContractId (Marlowe id) and TxId (Cardano id) of the submitted Tx + * @throws DecodingError + */ + createContract(createContractRequest: CreateContractRequest): Promise<[ContractId, TxId]>; + + /** + * Submit to the Cardano Ledger, the Transaction(Tx) that will apply inputs to a given created contract. + * @param contractId Contract Id where inputs will be applied + * @param applyInputsRequest inputs to apply + * @throws DecodingError + */ + applyInputs(contractId: ContractId, applyInputsRequest: ApplyInputsRequest): Promise; + + /** + * @deprecated Deprecated in favour of {@link @marlowe.io/runtime-lifecycle!api.ApplicableActionsAPI} + */ + getApplicableInputs(contractId: ContractId, environement: Environment): Promise; + + /** + * @description + * Fetches all contract ids for contracts on chain that mentions an address in your wallet. + * @throws Error | DecodingError + */ + getContractIds(): Promise; + + /** + * Get a list of the applied inputs for a given contract + * @param contractId + */ + getInputHistory(contractId: ContractId): Promise; +} export function mkContractLifecycle( wallet: WalletAPI, @@ -43,13 +310,13 @@ export function mkContractLifecycle( const di = { wallet, deprecatedRestAPI, restClient }; return { createContract: createContract(di), - applyInputs: applyInputsTx(di), + applyInputs: applyInputs(di), getApplicableInputs: getApplicableInputs(di), getContractIds: getContractIds(di), getInputHistory: getInputHistory(di), }; } -const getInputHistory = +export const getInputHistory = ({ restClient }: ContractsDI) => async (contractId: ContractId): Promise => { const transactionHeaders = await restClient.getTransactionsForContract({ @@ -102,14 +369,14 @@ const getInputHistory = .flat(); }; -const createContract = +export const createContract = ({ wallet, restClient }: ContractsDI) => async (createContractRequest: CreateContractRequest): Promise<[ContractId, TxId]> => { const addressesAndCollaterals = await getAddressesAndCollaterals(wallet); const baseRequest: BuildCreateContractTxRequestOptions = { version: "v1", - + accounts: createContractRequest.accountDeposits ?? {}, changeAddress: addressesAndCollaterals.changeAddress, usedAddresses: addressesAndCollaterals.usedAddresses, collateralUTxOs: addressesAndCollaterals.collateralUTxOs, @@ -151,14 +418,8 @@ const createContract = return [contractId, contractIdToTxId(contractId)]; }; -const applyInputsTx = - ({ wallet, deprecatedRestAPI }: ContractsDI) => - async (contractId: ContractId, applyInputsRequest: ApplyInputsRequest): Promise => { - return unsafeTaskEither(applyInputsTxFpTs(deprecatedRestAPI)(wallet)(contractId)(applyInputsRequest)); - }; - const getApplicableInputs = - ({ wallet, deprecatedRestAPI }: ContractsDI) => + ({ wallet, deprecatedRestAPI }: ContractsDI & DeprecatedRestDI) => async (contractId: ContractId, environement: Environment): Promise => { const contractDetails = await unsafeTaskEither(deprecatedRestAPI.contracts.contract.get(contractId)); if (!contractDetails.state) { @@ -170,7 +431,7 @@ const getApplicableInputs = }; const getContractIds = - ({ deprecatedRestAPI, wallet }: ContractsDI) => + ({ deprecatedRestAPI, wallet }: ContractsDI & DeprecatedRestDI) => async (): Promise => { const partyAddresses = [await wallet.getChangeAddress(), ...(await wallet.getUsedAddresses())]; const kwargs = { tags: [], partyAddresses, partyRoles: [] }; @@ -200,54 +461,28 @@ const getParties: (walletApi: WalletAPI) => (roleTokenMintingPolicyId: PolicyId) return roles.concat([changeAddress]).concat(usedAddresses); }; -export const applyInputsTxFpTs: ( - client: FPTSRestAPI -) => ( - wallet: WalletAPI -) => ( - contractId: ContractId -) => (applyInputsRequest: ApplyInputsRequest) => TE.TaskEither = - (client) => (wallet) => (contractId) => (applyInputsRequest) => - pipe( - tryCatchDefault(() => getAddressesAndCollaterals(wallet)), - TE.chain((addressesAndCollaterals: AddressesAndCollaterals) => - client.contracts.contract.transactions.post( - contractId, - { - inputs: applyInputsRequest.inputs, - version: "v1", - tags: applyInputsRequest.tags ? applyInputsRequest.tags : {}, - metadata: applyInputsRequest.metadata ? applyInputsRequest.metadata : {}, - invalidBefore: applyInputsRequest.invalidBefore, - invalidHereafter: applyInputsRequest.invalidHereafter, - }, - addressesAndCollaterals - ) - ), - TE.chainW((transactionTextEnvelope: TransactionTextEnvelope) => - pipe( - tryCatchDefault(() => wallet.signTx(transactionTextEnvelope.tx.cborHex)), - TE.chain((hexTransactionWitnessSet: HexTransactionWitnessSet) => - client.contracts.contract.transactions.transaction.put( - contractId, - transactionTextEnvelope.transactionId, - hexTransactionWitnessSet - ) - ), - TE.map(() => transactionTextEnvelope.transactionId) - ) - ) - ); +export const applyInputs = + ({ wallet, restClient }: ContractsDI) => + async (contractId: ContractId, applyInputsRequest: ApplyInputsRequest): Promise => { + const addressesAndCollaterals = await getAddressesAndCollaterals(wallet); + const envelope = await restClient.applyInputsToContract({ + contractId, + changeAddress: addressesAndCollaterals.changeAddress, + usedAddresses: addressesAndCollaterals.usedAddresses, + collateralUTxOs: addressesAndCollaterals.collateralUTxOs, + inputs: applyInputsRequest.inputs, + invalidBefore: applyInputsRequest.invalidBefore, + invalidHereafter: applyInputsRequest.invalidHereafter, + version: "v1", + metadata: applyInputsRequest.metadata, + tags: applyInputsRequest.tags, + }); -export const applyInputsFpTs: ( - client: FPTSRestAPI -) => ( - wallet: WalletAPI -) => ( - contractId: ContractId -) => (applyInputsRequest: ApplyInputsRequest) => TE.TaskEither = - (client) => (wallet) => (contractId) => (applyInputsRequest) => - pipe( - applyInputsTxFpTs(client)(wallet)(contractId)(applyInputsRequest), - TE.chainW((txId) => tryCatchDefault(() => wallet.waitConfirmation(txId).then((_) => txId))) - ); + const signed = await wallet.signTx(envelope.tx.cborHex); + await restClient.submitContractTransaction({ + contractId, + transactionId: envelope.transactionId, + hexTransactionWitnessSet: signed, + }); + return envelope.transactionId; + }; diff --git a/packages/runtime/lifecycle/src/generic/new-contract-api.ts b/packages/runtime/lifecycle/src/generic/new-contract-api.ts new file mode 100644 index 00000000..073a6408 --- /dev/null +++ b/packages/runtime/lifecycle/src/generic/new-contract-api.ts @@ -0,0 +1,309 @@ +import { ContractId, PolicyId, TxId, contractIdToTxId } from "@marlowe.io/runtime-core"; +import { CreateContractRequest, createContract, getInputHistory } from "./contracts.js"; +import { SingleInputTx, TransactionSuccess } from "@marlowe.io/language-core-v1/semantics"; +import { ChosenNum, Contract, Environment, MarloweState } from "@marlowe.io/language-core-v1"; +import { RestDI } from "@marlowe.io/runtime-rest-client"; +import { WalletDI } from "@marlowe.io/wallet"; +import { + ApplicableAction, + ApplicableInput, + ApplyApplicableInputRequest, + CanAdvance, + CanChoose, + CanDeposit, + CanNotify, + ChainTipDI, + GetContinuationDI, +} from "./applicable-actions.js"; +import * as Applicable from "./applicable-actions.js"; + +/** + * + * @description Dependency Injection for the Contract API + * @hidden + */ +export type ContractsDI = WalletDI & RestDI & GetContinuationDI & ChainTipDI; + +/** + * This Interface provides capabilities for runnning a Contract over Cardano. + * @category New ContractsAPI + */ +export interface ContractsAPI { + /** + * Submit to the Cardano Ledger, the Transaction(Tx) that will create the Marlowe Contract passed in the request. + * It doesn't wait for the transaction to be confirmed on the Cardano blockchain. + * @param createContractRequest Request parameters for creating a Marlowe Contract on Cardano + * @returns A contract instance API that can be used to interact with the contract newly created + */ + create(request: CreateContractRequest): Promise; + + /** + * Load a contract instance API for a given contract id. + * @param id The contract id of the contract instance + * @returns A contract instance API that can be used to interact with the contract + */ + load(id: ContractId): Promise; +} + +/** + * This function creates a ContractsAPI instance. + * @param di Dependency Injection for the Contract API + * @returns ContractsAPI instance + */ +export function mkContractsAPI(di: ContractsDI): ContractsAPI { + // The ContractInstance API is stateful as it has some cache, so whenever + // possible we want to reuse the same instance of the API for the same contractId + const apis = new Map(); + + return { + create: async (request) => { + const [contractId, _] = await createContract(di)(request); + apis.set(contractId, mkContractInstanceAPI(di, contractId)); + return apis.get(contractId)!; + }, + load: async (contractId) => { + if (apis.has(contractId)) { + return apis.get(contractId)!; + } else { + return mkContractInstanceAPI(di, contractId); + } + }, + }; +} + +/** + * This Interface provides capabilities for evaluating the applicable actions for a contract instance. + * An applicable action is an action that can be applied to a contract instance in a given environment. + * @category New ContractsAPI + **/ +export interface ApplicableActionsAPI { + /** + * A list of all the applicable actions for the contract instance regarless of the role of the wallet + * @returns A list of applicable actions + */ + actions: ApplicableAction[]; + /** + * A list of all the applicable actions for the contract instance that the wallet can perform + * @returns A list of applicable actions + */ + myActions: ApplicableAction[]; + /** + * Convert an applicable action to an applicable input + * @param action An applicable action + * @returns An applicable input + */ + toInput(action: CanNotify | CanDeposit | CanAdvance): Promise; + /** + * Convert an applicable action to an applicable input + * @param action An applicable action + * @param chosenNum The number chosen in the choice action + * @returns An applicable input + */ + toInput(action: CanChoose, chosenNum: ChosenNum): Promise; + /** + * Simulates the result of applying an input to the contract instance + * @param input An applicable input + * @returns The result of applying the input + */ + simulate(input: ApplicableInput): TransactionSuccess; + /** + * Apply an input to the contract instance + * @param req Request parameters for applying an input to the contract instance + * @returns The transaction id of the transaction that applied the input + */ + apply(req: ApplyApplicableInputRequest): Promise; +} + +function mkApplicableActionsAPI( + di: RestDI & WalletDI & GetContinuationDI & ChainTipDI, + actions: ApplicableAction[], + myActions: ApplicableAction[], + contractDetails: ContractDetails, + contractId: ContractId +): ApplicableActionsAPI { + const getActiveContractDetails = () => { + if (contractDetails.type !== "active") { + throw new Error("Contract is not active"); + } + return contractDetails; + }; + + const standaloneAPI = Applicable.mkApplicableActionsAPI(di); + + async function toInput(action: CanNotify | CanDeposit | CanAdvance): Promise; + async function toInput(action: CanChoose, chosenNum: ChosenNum): Promise; + async function toInput(action: ApplicableAction, chosenNum?: ChosenNum): Promise { + const activeContractDetails = getActiveContractDetails(); + if (action.type === "Choice") { + return standaloneAPI.getInput(activeContractDetails, action, chosenNum!); + } else { + return standaloneAPI.getInput(activeContractDetails, action); + } + } + + return { + actions, + myActions, + toInput, + simulate: (input) => { + const activeContractDetails = getActiveContractDetails(); + return standaloneAPI.simulateInput(activeContractDetails, input); + }, + apply: (req) => standaloneAPI.applyInput(contractId, req), + }; +} + +/** + * Request parameters for evaluating the applicable actions for a contract instance. + * @category New ContractsAPI + */ +export type EvaluateApplicableActionsRequest = { + /* + * Applicable actions are evaluated in the context of an environment (time interval of execution on the Cardano Ledger). + */ + environment?: Environment; +}; + +/** + * This Interface provides capabilities for interacting with a Contract Instance. + * A Contract Instance is a contract that has been created on the Cardano blockchain. + * @category New ContractsAPI + */ +export interface ContractInstanceAPI { + /** + * The contract Id of the contract instance + */ + id: ContractId; + /** + * Wait for the transaction that created the contract to be confirmed on the Cardano blockchain. + * @returns A boolean value indicating whether the transaction has been confirmed or not. + */ + waitForConfirmation: () => Promise; + /** + * Get the details of the contract instance + * @returns The details of the contract instance + */ + getDetails: () => Promise; + /** + * Check if the contract instance is active + * @returns A boolean value indicating whether the contract instance is active or not + */ + isActive: () => Promise; + /** + * Check if the contract instance is closed + * @returns A boolean value indicating whether the contract instance is closed or not + */ + isClosed: () => Promise; + /** + * Apply inputs to the contract instance + * @param applyInputsRequest Request parameters for applying inputs to the contract instance + * @returns The transaction id of the transaction that applied the inputs + */ + applyInput(request: ApplyApplicableInputRequest): Promise; + /** + * Compute the applicable actions for the contract instance + * @param request Request parameters for computing the applicable actions + * @returns ApplicableActionsAPI instance + */ + evaluateApplicableActions(request?: EvaluateApplicableActionsRequest): Promise; + /** + * Get a list of the applied inputs for the contract + */ + getInputHistory(): Promise; +} + +function mkContractInstanceAPI(di: ContractsDI & GetContinuationDI & ChainTipDI, id: ContractId): ContractInstanceAPI { + const contractCreationTxId = contractIdToTxId(id); + const applicableActionsAPI = Applicable.mkApplicableActionsAPI(di); + return { + id, + waitForConfirmation: async () => { + try { + // Todo : This is a temporary solution. We need to implement a better way to get the last transaction. + // Improve Error handling + const txs = await di.restClient.getTransactionsForContract({ contractId: id }).then((res) => res.transactions); + if (txs.length === 0) { + return di.wallet.waitConfirmation(contractCreationTxId); + } else { + return di.wallet.waitConfirmation(txs[txs.length - 1].transactionId); + } + } catch (e) { + // triggered when the contract is not found yet + return di.wallet.waitConfirmation(contractCreationTxId); + } + }, + getDetails: async () => { + return getContractDetails(di)(id); + }, + evaluateApplicableActions: async (req = {}) => { + const contractDetails = await getContractDetails(di)(id); + const actions = await applicableActionsAPI.getApplicableActions(contractDetails, req.environment); + let myActions = [] as ApplicableAction[]; + if (contractDetails.type === "active") { + const myActionsFilter = await applicableActionsAPI.mkFilter(contractDetails); + myActions = actions.filter(myActionsFilter); + } + + return mkApplicableActionsAPI(di, actions, myActions, contractDetails, id); + }, + applyInput: async (request) => { + return applicableActionsAPI.applyInput(id, request); + }, + isActive: async () => { + return (await getContractDetails(di)(id)).type === "active"; + }, + isClosed: async () => { + return (await getContractDetails(di)(id)).type === "closed"; + }, + getInputHistory: async () => { + // TODO: We can optimize this by only asking for the new transaction headers + // and only asking for contract details of the new transactions. + return getInputHistory(di)(id); + }, + }; +} + +function getContractDetails(di: ContractsDI) { + return async function (contractId: ContractId): Promise { + const contractDetails = await di.restClient.getContractById({ contractId }); + if (typeof contractDetails.state === "undefined" || typeof contractDetails.currentContract === "undefined") { + return { type: "closed" }; + } else { + return { + type: "active", + contractId, + currentState: contractDetails.state, + currentContract: contractDetails.currentContract, + roleTokenMintingPolicyId: contractDetails.roleTokenMintingPolicyId, + }; + } + }; +} + +/** + * A closed contract is a contract that has been closed and is no longer active. + * It is still stored in the Cardano ledger, but it cannot receive inputs or advance its state anymore. + * @category New ContractsAPI + */ +export type ClosedContract = { + type: "closed"; +}; + +/** + * An active contract is a contract appended to a Cardano ledger that is not closed. + * It can receive inputs and advance its state. + * @category New ContractsAPI + */ +export type ActiveContract = { + type: "active"; + contractId: ContractId; + currentState: MarloweState; + currentContract: Contract; + roleTokenMintingPolicyId: PolicyId; +}; + +/** + * Represents the details of a contract, either active or closed. + * @category New ContractsAPI + */ +export type ContractDetails = ClosedContract | ActiveContract; diff --git a/packages/runtime/lifecycle/src/generic/runtime.ts b/packages/runtime/lifecycle/src/generic/runtime.ts index 38562fd2..deadfe3a 100644 --- a/packages/runtime/lifecycle/src/generic/runtime.ts +++ b/packages/runtime/lifecycle/src/generic/runtime.ts @@ -1,11 +1,13 @@ -import { RuntimeLifecycle } from "../api.js"; import { WalletAPI } from "@marlowe.io/wallet/api"; - import { FPTSRestAPI, RestClient } from "@marlowe.io/runtime-rest-client"; +import { Contract } from "@marlowe.io/language-core-v1"; +import { ContractSourceId } from "@marlowe.io/marlowe-object"; +import { RuntimeLifecycle } from "../api.js"; import { mkPayoutLifecycle } from "./payouts.js"; import { mkContractLifecycle } from "./contracts.js"; import { mkApplicableActionsAPI } from "./applicable-actions.js"; +import * as NewContract from "./new-contract-api.js"; export function mkRuntimeLifecycle( deprecatedRestAPI: FPTSRestAPI, @@ -13,11 +15,34 @@ export function mkRuntimeLifecycle( wallet: WalletAPI ): RuntimeLifecycle { const contracts = mkContractLifecycle(wallet, deprecatedRestAPI, restClient); + // We cache the contract sources when the API is asking for continuations + const sources = new Map(); + + const di = { + wallet, + restClient, + getContractContinuation: async (contractSourceId: ContractSourceId) => { + if (sources.has(contractSourceId)) { + return sources.get(contractSourceId)!; + } else { + const contract = await restClient.getContractSourceById({ + contractSourceId, + }); + sources.set(contractSourceId, contract); + return contract; + } + }, + getRuntimeTip: async () => { + const status = await restClient.healthcheck(); + return new Date(status.tips.runtimeChain.slotTimeUTC); + }, + }; return { wallet: wallet, restClient, contracts, + newContractAPI: NewContract.mkContractsAPI(di), payouts: mkPayoutLifecycle(wallet, deprecatedRestAPI, restClient), - applicableActions: mkApplicableActionsAPI(restClient, wallet, contracts), + applicableActions: mkApplicableActionsAPI(di), }; } diff --git a/packages/runtime/lifecycle/src/index.ts b/packages/runtime/lifecycle/src/index.ts index e3a04f7b..5e765b65 100644 --- a/packages/runtime/lifecycle/src/index.ts +++ b/packages/runtime/lifecycle/src/index.ts @@ -20,8 +20,8 @@ import { WalletAPI } from "@marlowe.io/wallet"; import * as Generic from "./generic/runtime.js"; import { mkFPTSRestClient, mkRestClient } from "@marlowe.io/runtime-rest-client"; import { RuntimeLifecycle } from "./api.js"; -import { InvalidTypeError, strictDynamicTypeCheck } from "@marlowe.io/adapter/io-ts"; - +import { dynamicAssertType } from "@marlowe.io/adapter/io-ts"; +import * as t from "io-ts/lib/index.js"; export * as Browser from "./browser/index.js"; /** @@ -40,11 +40,14 @@ export interface RuntimeLifecycleOptions { } /** - * Creates an instance of RuntimeLifecycle. - * @param options - * @category RuntimeLifecycle + * @hidden */ -export function mkRuntimeLifecycle(options: RuntimeLifecycleOptions): RuntimeLifecycle; +export const RuntimeLifecycleOptionsGuard: t.Type = t.type({ + runtimeURL: t.string, + // TODO: Create a shallow guard for the wallet that checks that all methods are present as t.function. + wallet: t.any, +}); + /** * Creates an instance of RuntimeLifecycle. * @param options @@ -52,9 +55,8 @@ export function mkRuntimeLifecycle(options: RuntimeLifecycleOptions): RuntimeLif * @category RuntimeLifecycle */ export function mkRuntimeLifecycle(options: RuntimeLifecycleOptions, strict = true): RuntimeLifecycle { - if (!strictDynamicTypeCheck(strict)) { - throw new InvalidTypeError([], `Invalid type for argument 'strict', expected boolean but got ${strict}`); - } + dynamicAssertType(RuntimeLifecycleOptionsGuard, options); + dynamicAssertType(t.boolean, strict, "Invalid type for argument 'strict', expected boolean"); const { runtimeURL, wallet } = options; const deprecatedRestAPI = mkFPTSRestClient(runtimeURL); const restClient = mkRestClient(runtimeURL, strict); diff --git a/packages/runtime/lifecycle/src/nodejs/index.ts b/packages/runtime/lifecycle/src/nodejs/index.ts index 23403d52..cbc4a8cb 100644 --- a/packages/runtime/lifecycle/src/nodejs/index.ts +++ b/packages/runtime/lifecycle/src/nodejs/index.ts @@ -3,14 +3,11 @@ import * as Wallet from "@marlowe.io/wallet/lucid"; import * as Generic from "../generic/runtime.js"; import { RuntimeLifecycle } from "../api.js"; import { Lucid } from "lucid-cardano"; -import { InvalidTypeError, strictDynamicTypeCheck } from "@marlowe.io/adapter/io-ts"; +import * as t from "io-ts/lib/index.js"; +import { dynamicAssertType } from "@marlowe.io/adapter/io-ts"; -export async function mkRuntimeLifecycle(runtimeURL: string, lucid: Lucid): Promise; -export async function mkRuntimeLifecycle(runtimeURL: string, lucid: Lucid, strict: boolean): Promise; -export async function mkRuntimeLifecycle(runtimeURL: string, lucid: Lucid, strict: unknown = true) { - if (!strictDynamicTypeCheck(strict)) { - throw new InvalidTypeError([], `Invalid type for argument 'strict', expected boolean but got ${strict}`); - } +export async function mkRuntimeLifecycle(runtimeURL: string, lucid: Lucid, strict = true): Promise { + dynamicAssertType(t.boolean, strict, "Invalid type for argument 'strict', expected boolean"); const wallet = await Wallet.mkLucidWallet(lucid); const deprecatedRestAPI = mkFPTSRestClient(runtimeURL); const restClient = mkRestClient(runtimeURL, strict); diff --git a/packages/runtime/lifecycle/test/examples/swap.ada.token.e2e.spec.ts b/packages/runtime/lifecycle/test/examples/swap.ada.token.e2e.spec.ts index 6bc97c55..b893b5ae 100644 --- a/packages/runtime/lifecycle/test/examples/swap.ada.token.e2e.spec.ts +++ b/packages/runtime/lifecycle/test/examples/swap.ada.token.e2e.spec.ts @@ -1,13 +1,12 @@ import { pipe } from "fp-ts/lib/function.js"; import { addDays } from "date-fns"; -import { datetoTimeout, Input, MarloweState } from "@marlowe.io/language-core-v1"; +import { datetoTimeout } from "@marlowe.io/language-core-v1"; import console from "console"; -import { ContractId, payoutId, runtimeTokenToMarloweTokenValue } from "@marlowe.io/runtime-core"; +import { mkaccountDeposits, runtimeTokenToMarloweTokenValue } from "@marlowe.io/runtime-core"; import { MINUTES } from "@marlowe.io/adapter/time"; import { AtomicSwap } from "@marlowe.io/language-examples"; -import { RestClient } from "@marlowe.io/runtime-rest-client"; import { generateSeedPhrase, logDebug, @@ -19,9 +18,18 @@ import { } from "@marlowe.io/testing-kit"; import { AxiosError } from "axios"; import { MarloweJSON } from "@marlowe.io/adapter/codec"; -import { onlyByContractIds, RuntimeLifecycle } from "@marlowe.io/runtime-lifecycle/api"; +import { + CanDeposit, + CanNotify, + ContractInstanceAPI, + CreateContractRequest, + CreateContractRequestFromContract, + RuntimeLifecycle, + onlyByContractIds, +} from "@marlowe.io/runtime-lifecycle/api"; import { mintRole } from "@marlowe.io/runtime-rest-client/contract"; +import { computeTransaction, emptyState, reduceContractUntilQuiescent } from "@marlowe.io/language-core-v1/semantics"; global.console = console; @@ -55,7 +63,7 @@ describe("swap", () => { await logWalletInfo("buyer", buyer.wallet); const sellerLifecycle = mkLifecycle(seller.wallet); const buyerLifecycle = mkLifecycle(buyer.wallet); - const bankLifecycle = mkLifecycle(bank); + const anyoneLifecycle = mkLifecycle(bank); const scheme: AtomicSwap.Scheme = { offer: { @@ -77,69 +85,56 @@ describe("swap", () => { logDebug(`contract: ${MarloweJSON.stringify(swapContract, null, 4)}`); logInfo("Contract Creation"); - const openroleCOnfig = mintRole("OpenRole"); - logInfo(`Config ${MarloweJSON.stringify(openroleCOnfig, null, 4)}`); - const [contractId, txCreatedContract] = await sellerLifecycle.contracts.createContract({ + const sellerContractInstance = await sellerLifecycle.newContractAPI.create({ contract: swapContract, - minimumLovelaceUTxODeposit: 3_000_000, - roles: { - [scheme.ask.buyer.role_token]: openroleCOnfig, - }, + roles: { [scheme.ask.buyer.role_token]: mintRole("OpenRole") }, + accountDeposits: mkaccountDeposits([[scheme.offer.seller, seller.assetsProvisioned]]), }); - logInfo("Contract Created"); - await seller.wallet.waitConfirmation(txCreatedContract); - await seller.wallet.waitRuntimeSyncingTillCurrentWalletTip(sellerLifecycle.restClient); - logInfo(`Seller > Provision Offer`); - - let actions = await getAvailableActions(sellerLifecycle, scheme, contractId); - - expect(actions.length).toBe(1); - expect(actions[0].typeName).toBe("ProvisionOffer"); - const provisionOffer = actions[0] as AtomicSwap.ProvisionOffer; - const offerProvisionedTx = await sellerLifecycle.contracts.applyInputs(contractId, { - inputs: [provisionOffer.input], - }); - - await seller.wallet.waitConfirmation(offerProvisionedTx); + sellerContractInstance.waitForConfirmation(); await seller.wallet.waitRuntimeSyncingTillCurrentWalletTip(sellerLifecycle.restClient); + expect(await sellerContractInstance.isActive()).toBeTruthy(); + + logInfo(`contract created : ${sellerContractInstance.id}`); logInfo(`Buyer > Swap`); - actions = await getAvailableActions(sellerLifecycle, scheme, contractId); + const buyerContractInstance = await buyerLifecycle.newContractAPI.load(sellerContractInstance.id); + let applicableActions = await buyerContractInstance.evaluateApplicableActions(); + // N.B : the applicable actions' API is not able yet to handle Open Roles in the contract properly + // so myactions will be empty in that case and we need to use actions instead and manually select + // the action Desposit we want to apply. + expect(applicableActions.actions.map((a) => a.type)).toStrictEqual(["Deposit", "Choice"]); - expect(actions.length).toBe(2); - expect(actions[0].typeName).toBe("Swap"); - expect(actions[1].typeName).toBe("Retract"); - const swap = actions[0] as AtomicSwap.Swap; - const swappedTx = await buyerLifecycle.contracts.applyInputs(contractId, { inputs: [swap.input] }); + const swap = await applicableActions.toInput(applicableActions.actions[0] as CanDeposit); - await buyer.wallet.waitConfirmation(swappedTx); - await buyer.wallet.waitRuntimeSyncingTillCurrentWalletTip(buyerLifecycle.restClient); + await buyerContractInstance.applyInput({ input: swap }); - logInfo(`Anyone > Confirm Swap`); + await buyerContractInstance.waitForConfirmation(); + await buyer.wallet.waitRuntimeSyncingTillCurrentWalletTip(sellerLifecycle.restClient); - actions = await getAvailableActions(bankLifecycle, scheme, contractId); + logInfo(`Anyone > Confirm Swap`); - expect(actions.length).toBe(1); - expect(actions[0].typeName).toBe("ConfirmSwap"); - const confirmSwap = actions[0] as AtomicSwap.ConfirmSwap; - const swapConfirmedTx = await bankLifecycle.contracts.applyInputs(contractId, { inputs: [confirmSwap.input] }); + const anyoneContractInstance = await anyoneLifecycle.newContractAPI.load(sellerContractInstance.id); + applicableActions = await buyerContractInstance.evaluateApplicableActions(); + expect(applicableActions.myActions.map((a) => a.type)).toStrictEqual(["Notify"]); + const swapConfirmation = await applicableActions.toInput(applicableActions.myActions[0] as CanNotify); - await bank.waitConfirmation(swapConfirmedTx); - await bank.waitRuntimeSyncingTillCurrentWalletTip(bankLifecycle.restClient); + await anyoneContractInstance.applyInput({ input: swapConfirmation }); - const closedState = await getClosedState(bankLifecycle, scheme, contractId); + await anyoneContractInstance.waitForConfirmation(); + await buyer.wallet.waitRuntimeSyncingTillCurrentWalletTip(sellerLifecycle.restClient); - expect(closedState.reason.typeName).toBe("Swapped"); + expect(anyoneContractInstance.isClosed).toBeTruthy(); logInfo(`Buyer > Retrieve Payout`); - const buyerPayouts = await buyerLifecycle.payouts.available(onlyByContractIds([contractId])); - expect(buyerPayouts.length).toBe(1); + const buyerPayouts = await buyerLifecycle.payouts.available(onlyByContractIds([buyerContractInstance.id])); + expect(buyerPayouts.length).toStrictEqual(1); await buyerLifecycle.payouts.withdraw([buyerPayouts[0].payoutId]); + logInfo(`Swapped Completed`); await logWalletInfo("seller", seller.wallet); await logWalletInfo("buyer", buyer.wallet); } catch (e) { @@ -152,45 +147,3 @@ describe("swap", () => { 10 * MINUTES ); }); - -const getClosedState = async ( - runtimeLifecycle: RuntimeLifecycle, - scheme: AtomicSwap.Scheme, - contractId: ContractId -): Promise => { - const inputHistory = await runtimeLifecycle.contracts.getInputHistory(contractId); - - await shouldBeAClosedContract(runtimeLifecycle.restClient, contractId); - - return AtomicSwap.getClosedState(scheme, inputHistory); -}; - -const getAvailableActions = async ( - runtimeLifecycle: RuntimeLifecycle, - scheme: AtomicSwap.Scheme, - contractId: ContractId -): Promise => { - const inputHistory = await runtimeLifecycle.contracts.getInputHistory(contractId); - - const marloweState = await getMarloweStatefromAnActiveContract(runtimeLifecycle.restClient, contractId); - const now = datetoTimeout(new Date()); - const contractState = AtomicSwap.getActiveState(scheme, now, inputHistory, marloweState); - return AtomicSwap.getAvailableActions(scheme, contractState); -}; - -const shouldBeAClosedContract = async (restClient: RestClient, contractId: ContractId): Promise => { - const state = await restClient.getContractById({ contractId }).then((contractDetails) => contractDetails.state); - if (state) { - throw new Error("Contract retrieved is not Closed"); - } else { - return; - } -}; - -const shouldBeAnActiveContract = (state?: MarloweState): MarloweState => { - if (state) return state; - else throw new Error("Contract retrieved is not Active"); -}; - -const getMarloweStatefromAnActiveContract = (restClient: RestClient, contractId: ContractId): Promise => - restClient.getContractById({ contractId }).then((contractDetails) => shouldBeAnActiveContract(contractDetails.state)); diff --git a/packages/runtime/lifecycle/test/generic/contracts.e2e.spec.ts b/packages/runtime/lifecycle/test/generic/contracts.e2e.spec.ts index 7a73102b..39185e71 100644 --- a/packages/runtime/lifecycle/test/generic/contracts.e2e.spec.ts +++ b/packages/runtime/lifecycle/test/generic/contracts.e2e.spec.ts @@ -7,6 +7,12 @@ import { oneNotifyTrue } from "@marlowe.io/language-examples"; import console from "console"; import { MINUTES } from "@marlowe.io/adapter/time"; import { logError, logInfo, mkTestEnvironment, readTestConfiguration } from "@marlowe.io/testing-kit"; +import { + CanAdvance, + CanNotify, + ContractInstanceAPI, + CreateContractRequest, +} from "@marlowe.io/runtime-lifecycle/api.js"; global.console = console; @@ -16,15 +22,26 @@ describe("Runtime Contract Lifecycle ", () => { async () => { try { const { bank, mkLifecycle } = await readTestConfiguration().then(mkTestEnvironment({})); - const runtimeLifecycle = mkLifecycle(bank); - const [contractId, txIdContractCreated] = await runtimeLifecycle.contracts.createContract({ - contract: close, - minimumLovelaceUTxODeposit: 3_000_000, - }); - await bank.waitConfirmation(txIdContractCreated); - logInfo(`contractID created : ${contractId}`); + const runtime = mkLifecycle(bank); + const contractInstance = await runtime.newContractAPI.create({ contract: close }); + await contractInstance.waitForConfirmation(); + await bank.waitRuntimeSyncingTillCurrentWalletTip(runtime.restClient); + + logInfo(`contract created : ${contractInstance.id}`); + // N.B : This particular Close contract needs to be reduced/advanced to be closed + expect(await contractInstance.isActive()).toBeTruthy(); + let applicableActions = await contractInstance.evaluateApplicableActions(); + + expect(applicableActions.myActions.map((a) => a.type)).toStrictEqual(["Advance"]); + const inputAdvance = await applicableActions.toInput(applicableActions.myActions[0] as CanAdvance); + + await contractInstance.applyInput({ input: inputAdvance }); + await contractInstance.waitForConfirmation(); + await bank.waitRuntimeSyncingTillCurrentWalletTip(runtime.restClient); + expect(await contractInstance.isClosed()).toBeTruthy(); } catch (e) { const error = e as AxiosError; + logError("An error occurred while creating a contract"); logError(JSON.stringify(error.response?.data)); logError(JSON.stringify(error)); expect(true).toBe(false); @@ -41,20 +58,22 @@ describe("Runtime Contract Lifecycle ", () => { const runtime = mkLifecycle(bank); const notifyTimeout = pipe(addDays(Date.now(), 1), datetoTimeout); - const [contractId, txIdContractCreated] = await runtime.contracts.createContract({ - contract: oneNotifyTrue(notifyTimeout), - minimumLovelaceUTxODeposit: 3_000_000, - }); - await bank.waitConfirmation(txIdContractCreated); - logInfo( - `contractID status : ${contractId} -> ${(await runtime.restClient.getContractById({ contractId })).status}` - ); + const contractInstance = await runtime.newContractAPI.create({ contract: oneNotifyTrue(notifyTimeout) }); + await contractInstance.waitForConfirmation(); + await bank.waitRuntimeSyncingTillCurrentWalletTip(runtime.restClient); + logInfo(`contract created : ${contractInstance.id}`); + expect(await contractInstance.isActive()).toBeTruthy(); + + let applicableActions = await contractInstance.evaluateApplicableActions(); + + expect(applicableActions.myActions.map((a) => a.type)).toStrictEqual(["Notify"]); + const inputNotify = await applicableActions.toInput(applicableActions.myActions[0] as CanNotify); + + await contractInstance.applyInput({ input: inputNotify }); + await contractInstance.waitForConfirmation(); await bank.waitRuntimeSyncingTillCurrentWalletTip(runtime.restClient); - const txIdInputsApplied = await runtime.contracts.applyInputs(contractId, { - inputs: [inputNotify], - }); - const result = await bank.waitConfirmation(txIdInputsApplied); - expect(result).toBe(true); + + expect(await contractInstance.isClosed()).toBeTruthy(); } catch (e) { const error = e as AxiosError; logError(error.message); diff --git a/packages/runtime/lifecycle/test/generic/payouts.e2e.spec.ts b/packages/runtime/lifecycle/test/generic/payouts.e2e.spec.ts deleted file mode 100644 index 4e6fdd06..00000000 --- a/packages/runtime/lifecycle/test/generic/payouts.e2e.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { pipe } from "fp-ts/lib/function.js"; -import { addDays } from "date-fns"; - -import { AtomicSwap } from "@marlowe.io/language-examples"; -import { datetoTimeout, adaValue } from "@marlowe.io/language-core-v1"; -import { Deposit } from "@marlowe.io/language-core-v1/next"; - -import console from "console"; -import { runtimeTokenToMarloweTokenValue } from "@marlowe.io/runtime-core"; -import { onlyByContractIds } from "@marlowe.io/runtime-lifecycle/api"; -import { MINUTES } from "@marlowe.io/adapter/time"; -import { mintRole } from "@marlowe.io/runtime-rest-client/contract"; - -global.console = console; - -describe.skip("Payouts", () => { - // const provisionScheme = { - // provider: { adaAmount: 20_000_000n }, - // swapper: { adaAmount: 20_000_000n, tokenAmount: 50n, tokenName: "TokenA" }, - // }; - - // async function executeSwapWithRequiredWithdrawalTillClosing() { - // const { tokenValueMinted, runtime, adaProvider, tokenProvider } = - // await provisionAnAdaAndTokenProvider( - // getMarloweRuntimeUrl(), - // getBlockfrostContext(), - // getBankPrivateKey(), - // provisionScheme - // ); - // const scheme: AtomicSwap.Scheme = { - // offer: { - // seller: { address: adaProvider.address }, - // deadline: pipe(addDays(Date.now(), 1), datetoTimeout), - // asset: adaValue(2n), - // }, - // ask: { - // buyer: { role_token: "buyer" }, - // deadline: pipe(addDays(Date.now(), 1), datetoTimeout), - // asset: runtimeTokenToMarloweTokenValue(tokenValueMinted), - // }, - // swapConfirmation: { - // deadline: pipe(addDays(Date.now(), 1), datetoTimeout), - // }, - // }; - - // const swapContract = AtomicSwap.mkContract(scheme); - // const [contractId, txCreatedContract] = await runtime( - // adaProvider - // ).contracts.createContract({ - // contract: swapContract, - // roles: { [scheme.ask.buyer.role_token]: mintRole(tokenProvider.address) }, - // }); - - // await runtime(adaProvider).wallet.waitConfirmation(txCreatedContract); - - // // Applying the first Deposit - // let next = await runtime(adaProvider).contracts.getApplicableInputs( - // contractId - // ); - // const txFirstTokensDeposited = await runtime( - // adaProvider - // ).contracts.applyInputs(contractId, { - // inputs: [pipe(next.applicable_inputs.deposits[0], Deposit.toInput)], - // }); - // await runtime(adaProvider).wallet.waitConfirmation(txFirstTokensDeposited); - - // // Applying the second Deposit - // next = await runtime(tokenProvider).contracts.getApplicableInputs( - // contractId - // ); - // await runtime(tokenProvider).contracts.applyInputs(contractId, { - // inputs: [pipe(next.applicable_inputs.deposits[0], Deposit.toInput)], - // }); - // await runtime(tokenProvider).wallet.waitConfirmation( - // txFirstTokensDeposited - // ); - - // return { - // contractId, - // runtime, - // adaProvider, - // tokenProvider, - // }; - // } - - it( - "Payouts can be withdrawn", - async () => { - // const result = await executeSwapWithRequiredWithdrawalTillClosing(); - // const { adaProvider, tokenProvider, contractId, runtime } = result; - // const adaProviderPayouts = await runtime(adaProvider).payouts.available( - // onlyByContractIds([contractId]) - // ); - // expect(adaProviderPayouts.length).toBe(1); - // await runtime(adaProvider).payouts.withdraw([ - // adaProviderPayouts[0].payoutId, - // ]); - // const tokenProviderPayouts = await runtime( - // tokenProvider - // ).payouts.available(onlyByContractIds([contractId])); - // expect(tokenProviderPayouts.length).toBe(1); - // await runtime(tokenProvider).payouts.withdraw([ - // tokenProviderPayouts[0].payoutId, - // ]); - }, - 10 * MINUTES - ); -}); diff --git a/packages/testing-kit/src/environment/index.ts b/packages/testing-kit/src/environment/index.ts index f51c3ab3..8109c3dc 100644 --- a/packages/testing-kit/src/environment/index.ts +++ b/packages/testing-kit/src/environment/index.ts @@ -5,7 +5,14 @@ import { Lucid, Blockfrost } from "lucid-cardano"; import { RuntimeLifecycle } from "@marlowe.io/runtime-lifecycle/api"; import { Assets } from "@marlowe.io/runtime-core"; -import { TestConfiguration, logInfo, logWalletInfo, mkLucidWalletTest } from "@marlowe.io/testing-kit"; +import { + TestConfiguration, + logDebug, + logInfo, + logWalletInfo, + logWarning, + mkLucidWalletTest, +} from "@marlowe.io/testing-kit"; import { ProvisionRequest, WalletTestAPI } from "../wallet/api.js"; /** @@ -68,8 +75,9 @@ export const mkTestEnvironment = if (bankBalance <= 100_000_000n) { throw { message: "Bank is not sufficiently provisionned (< 100 Ada)" }; } - logInfo("Bank is provisionned enough"); + logDebug("Bank is provisionned enough to run tests"); const participants = await bank.provision(provisionRequest); + logDebug("Participants provisionned"); await bank.waitRuntimeSyncingTillCurrentWalletTip(mkRestClient(testConfiguration.runtimeURL)); logInfo("Test Environment : Ready"); diff --git a/packages/testing-kit/src/logging.ts b/packages/testing-kit/src/logging.ts index 26eaf031..33a83a06 100644 --- a/packages/testing-kit/src/logging.ts +++ b/packages/testing-kit/src/logging.ts @@ -2,7 +2,7 @@ import { MarloweJSON } from "@marlowe.io/adapter/codec"; import { WalletTestAPI } from "./wallet/api.js"; export const logDebug = (message: string) => - process.env.LOG_DEBUG_LEVEL !== undefined && JSON.parse(process.env.LOG_DEBUG_LEVEL) === true + process.env.LOG_DEBUG_LEVEL !== undefined && process.env.LOG_DEBUG_LEVEL === "1" ? console.log(`## ||| [${message}]`) : {}; diff --git a/packages/testing-kit/src/wallet/lucid/index.ts b/packages/testing-kit/src/wallet/lucid/index.ts index e40be5ab..053d9d16 100644 --- a/packages/testing-kit/src/wallet/lucid/index.ts +++ b/packages/testing-kit/src/wallet/lucid/index.ts @@ -11,7 +11,7 @@ import { Lucid } from "lucid-cardano"; import { WalletAPI, mkLucidWallet } from "@marlowe.io/wallet"; import { RestClient } from "@marlowe.io/runtime-rest-client"; -import { logInfo, logWarning } from "../../logging.js"; +import { logDebug, logInfo, logWarning } from "../../logging.js"; export * as Provision from "./provisionning.js"; import * as Provision from "./provisionning.js"; @@ -55,11 +55,13 @@ const waitRuntimeSyncingTillCurrentWalletTip = (di: WalletTestDI) => async (client: RestClient): Promise => { const { lucid } = di; + logInfo("Waiting for Runtime to sync with the Wallet"); const currentLucidSlot = BigInt(lucid.currentSlot()); - logInfo(`Setting up a synchronization point with Runtime at SlotNo ${currentLucidSlot}`); + logInfo(`Setting up a synchronization point with Runtime at slot ${currentLucidSlot}`); await waitForPredicatePromise(isRuntimeChainMoreAdvancedThan(client, currentLucidSlot)); - process.stdout.write("\n"); - return sleep(15); + logInfo(`Runtime and Wallet passsed both ${currentLucidSlot} slot.`); + // This sleep will be removed when we have a better tip for runtime... + return sleep(20); }; /** @@ -70,11 +72,13 @@ const waitRuntimeSyncingTillCurrentWalletTip = */ export const isRuntimeChainMoreAdvancedThan = (client: RestClient, aSlotNo: bigint) => () => client.healthcheck().then((status) => { + logDebug(`Runtime Chain Tip SlotNo : ${status.tips.runtimeChain.blockHeader.slotNo}`); + logDebug(`Wallet Chain Tip SlotNo : ${aSlotNo}`); if (status.tips.runtimeChain.blockHeader.slotNo >= aSlotNo) { return true; } else { const delta = aSlotNo - status.tips.runtimeChain.blockHeader.slotNo; - process.stdout.write(`Waiting Runtime to reach that point (${delta} slots behind (~${delta}s)) `); + logDebug(`Waiting Runtime to reach that point (${delta} slots behind (~${delta}s)) `); return false; } }); diff --git a/packages/testing-kit/src/wallet/lucid/provisionning.ts b/packages/testing-kit/src/wallet/lucid/provisionning.ts index d043bf69..3fd7ee17 100644 --- a/packages/testing-kit/src/wallet/lucid/provisionning.ts +++ b/packages/testing-kit/src/wallet/lucid/provisionning.ts @@ -132,7 +132,7 @@ const mkPolicyWithDeadlineAndOneAuthorizedSigner = const toAssetsToTransfer = (assets: RuntimeCore.Assets): LucidAssets => { var lucidAssets: { [key: string]: bigint } = {}; - lucidAssets["lovelace"] = assets.lovelaces; + lucidAssets["lovelace"] = assets.lovelaces ?? 0n; assets.tokens.map( (token) => (lucidAssets[toUnit(token.assetId.policyId, fromText(token.assetId.assetName))] = token.quantity) ); diff --git a/packages/wallet/src/lucid/index.ts b/packages/wallet/src/lucid/index.ts index 90036bb2..06d429cd 100644 --- a/packages/wallet/src/lucid/index.ts +++ b/packages/wallet/src/lucid/index.ts @@ -102,7 +102,7 @@ const waitConfirmation = try { return await lucid.awaitTx(txHash); } catch (reason) { - throw new Error(`Error while awiting : ${reason}`); + throw new Error(`Error while awaiting : ${reason}`); } }; /**