diff --git a/examples/nodejs/src/applicable-inputs.ts b/examples/nodejs/src/applicable-inputs.ts
index f6fbe85a..153e9b40 100644
--- a/examples/nodejs/src/applicable-inputs.ts
+++ b/examples/nodejs/src/applicable-inputs.ts
@@ -4,7 +4,6 @@ import {
Contract,
Deposit,
Choice,
- BuiltinByteString,
Input,
ChosenNum,
Environment,
@@ -14,11 +13,17 @@ import {
Case,
Action,
Notify,
+ IDeposit,
+ IChoice,
+ INotify,
+ InputContent,
} from "@marlowe.io/language-core-v1";
import {
- applyInput,
- ContractQuiescentReduceResult,
+ applyAllInputs,
convertReduceWarning,
+ evalObservation,
+ evalValue,
+ inBounds,
Payment,
reduceContractUntilQuiescent,
TransactionWarning,
@@ -28,130 +33,60 @@ import { RestClient } from "@marlowe.io/runtime-rest-client";
type ActionApplicant = Party | "anybody";
-interface CanNotify {
- type: "Notify";
+interface AppliedActionResult {
/**
- * Who can make the action
+ * What inputs needs to be provided to apply the action
*/
- applicant: "anybody";
+ inputs: Input[];
/**
- * If the Case is merkleized, this is the continuation hash
+ * What is the environment to apply the inputs
*/
- merkleizedContinuation?: BuiltinByteString;
+ environment: Environment;
/**
- * What is the new state after applying this action and reducing until quiescent
+ * What is the new state after applying an action and reducing until quiescent
*/
reducedState: MarloweState;
/**
- * What is the new contract after applying this action and reducing until quiescent
+ * What is the new contract after applying an action and reducing until quiescent
*/
reducedContract: Contract;
/**
- * What warnings were produced while applying this action
+ * What warnings were produced while applying an action
*/
warnings: TransactionWarning[];
/**
- * What payments were produced while applying this action
+ * What payments were produced while applying an action
*/
payments: Payment[];
+}
+
+interface CanNotify {
+ type: "Notify";
- toInput(): Promise;
+ applyAction(): AppliedActionResult;
}
interface CanDeposit {
type: "Deposit";
- /**
- * Who can make the action
- */
- applicant: ActionApplicant;
- /**
- * If the Case is merkleized, this is the continuation hash
- */
- merkleizedContinuation?: BuiltinByteString;
-
deposit: Deposit;
- /**
- * What is the new state after applying this action and reducing until quiescent
- */
- reducedState: MarloweState;
- /**
- * What is the new contract after applying this action and reducing until quiescent
- */
- reducedContract: Contract;
- /**
- * What warnings were produced while applying this action
- */
- warnings: TransactionWarning[];
- /**
- * What payments were produced while applying this action
- */
- payments: Payment[];
- toInput(): Promise;
+ applyAction(): AppliedActionResult;
}
interface CanChoose {
type: "Choice";
- /**
- * Who can make the action
- */
- applicant: ActionApplicant;
-
- /**
- * If the Case is merkleized, this is the continuation hash
- */
- merkleizedContinuation?: BuiltinByteString;
-
choice: Choice;
- /**
- * What is the new state after applying this action and reducing until quiescent
- */
- reducedState: MarloweState;
- /**
- * What is the new contract after applying this action and reducing until quiescent
- */
- reducedContract: Contract;
- /**
- * What warnings were produced while applying this action
- */
- warnings: TransactionWarning[];
- /**
- * What payments were produced while applying this action
- */
- payments: Payment[];
- toInput(choice: ChosenNum): Promise;
+ applyAction(choice: ChosenNum): AppliedActionResult;
}
interface CanAdvanceTimeout {
type: "AdvanceTimeout";
- /**
- * Who can make the action
- */
- applicant: "anybody";
-
- /**
- * What is the new state after applying this action and reducing until quiescent
- */
- reducedState: MarloweState;
- /**
- * What is the new contract after applying this action and reducing until quiescent
- */
- reducedContract: Contract;
- /**
- * What warnings were produced while applying this action
- */
- warnings: TransactionWarning[];
- /**
- * What payments were produced while applying this action
- */
- payments: Payment[];
-
- toInput(): Promise;
+ applyAction(): AppliedActionResult;
}
export type ApplicableAction =
@@ -160,6 +95,18 @@ export type ApplicableAction =
| CanChoose
| CanAdvanceTimeout;
+function getApplicant(action: ApplicableAction): ActionApplicant {
+ switch (action.type) {
+ case "Notify":
+ case "AdvanceTimeout":
+ return "anybody";
+ case "Deposit":
+ return action.deposit.party;
+ case "Choice":
+ return action.choice.for_choice.choice_owner;
+ }
+}
+
export async function getApplicableActions(
restClient: RestClient,
contractId: ContractId,
@@ -178,7 +125,11 @@ export async function getApplicableActions(
const timeInterval = { from: now, to: nextTimeout - 1n };
const env = environment ?? { timeInterval };
- if (contractDetails.state._tag == "None") throw new Error("State not set");
+ if (contractDetails.state._tag == "None") {
+ // TODO: Check, I believe this happens when a contract is in a closed state, but it would be nice
+ // if the API returned something more explicit.
+ return [];
+ }
const initialReduce = reduceContractUntilQuiescent(
env,
contractDetails.state.value,
@@ -189,30 +140,43 @@ export async function getApplicableActions(
if (initialReduce.reduced) {
applicableActions.push({
type: "AdvanceTimeout",
- applicant: "anybody",
- reducedState: initialReduce.state,
- reducedContract: initialReduce.continuation,
- warnings: convertReduceWarning(initialReduce.warnings),
- payments: initialReduce.payments,
- async toInput() {
- return [];
+ applyAction() {
+ return {
+ inputs: [],
+ environment: env,
+ reducedState: initialReduce.state,
+ reducedContract: initialReduce.continuation,
+ warnings: convertReduceWarning(initialReduce.warnings),
+ payments: initialReduce.payments,
+ };
},
});
}
-
- return applicableActions;
-}
-
-function getApplicableInputsFromReduction(
- initialReduce: ContractQuiescentReduceResult
-) {
const cont = initialReduce.continuation;
- if (cont == "close") return [];
+ if (cont === "close") return applicableActions;
if ("when" in cont) {
- // cont.when
+ const applicableActionsFromCases = await Promise.all(
+ cont.when.map((cse) =>
+ getApplicableActionFromCase(
+ restClient,
+ env,
+ initialReduce.continuation,
+ initialReduce.state,
+ initialReduce.payments,
+ convertReduceWarning(initialReduce.warnings),
+ cse
+ )
+ )
+ );
+ applicableActions = applicableActions.concat(applicableActionsFromCases.filter(x => x !== undefined) as ApplicableAction[]);
+
}
+
+
+ return applicableActions;
}
+
function isDepositAction(action: Action): action is Deposit {
return "party" in action;
}
@@ -225,29 +189,125 @@ function isChoice(action: Action): action is Choice {
return "choose_between" in action;
}
-async function getApplicableActionFromCase(restClient: RestClient, cse: Case) {
- // async function getApplicableActionFromCase(restClient: RestClient, cse: Case): ApplicableAction {
- let cont: Contract;
+async function getApplicableActionFromCase(
+ restClient: RestClient,
+ env: Environment,
+ currentContract: Contract,
+ state: MarloweState,
+ previousPayments: Payment[],
+ previousWarnings: TransactionWarning[],
+ cse: Case
+): Promise {
+ let cseContinuation: Contract;
if ("merkleized_then" in cse) {
- cont = await restClient.getContractSourceById({
+ cseContinuation = await restClient.getContractSourceById({
contractSourceId: cse.merkleized_then,
});
} else {
- cont = cse.then;
+ cseContinuation = cse.then;
+ }
+ function decorateInput(content: InputContent): Input {
+ if ("merkleized_then" in cse) {
+ const merkleizedHashAndContinuation = {
+ continuation_hash: cse.merkleized_then,
+ merkleized_continuation: cseContinuation
+ }
+ // MerkleizedNotify are serialized as the plain merkle object
+ if (content === "input_notify") {
+ return merkleizedHashAndContinuation;
+ } else {
+ // For IDeposit and IChoice is the InputContent + the merkle object
+ return {
+ ...merkleizedHashAndContinuation,
+ ...content
+ }
+ }
+ } else {
+ return content;
+ }
}
- // Para armar el input necesito el choice, para los warnings, payments y etc
- // necesito el input, eso significa que tengo que cambiar la firma para que el toInput devuelva el
- // input y los effects
+
if (isDepositAction(cse.case)) {
- applyInput(env, state, input, cont);
- // return {
- // applicant: cse.case.party,
- // type: "Deposit"
+ const deposit = cse.case;
+ return {
+ type: "Deposit",
+ deposit,
+
+ applyAction() {
+ const input = decorateInput({
+ input_from_party: deposit.party,
+ that_deposits: evalValue(env, state, deposit.deposits),
+ of_token: deposit.of_token,
+ into_account: deposit.into_account,
+ });
+ // TODO: Re-check if this env should be the same as the initial env or a new one.
+ const appliedInput = applyAllInputs(env, state, currentContract, [input]);
+
+ // TODO: Improve error handling
+ if (typeof appliedInput === "string") throw new Error(appliedInput);
+ return {
+ inputs: [input],
+ environment: env,
+ reducedState: appliedInput.state,
+ reducedContract: appliedInput.continuation,
+ warnings: [...previousWarnings, ...appliedInput.warnings],
+ payments: [...previousPayments, ...appliedInput.payments],
+ };
+ },
+ };
+ } else if (isChoice(cse.case)) {
+ const choice = cse.case;
+
+ return {
+ type: "Choice",
+ choice,
- // }
- } else if (isNotify(cse.case)) {
+ applyAction(chosenNum: ChosenNum) {
+ if (!inBounds(chosenNum, choice.choose_between)) {
+ throw new Error("Chosen number is not in bounds");
+ }
+ const input = decorateInput({
+ for_choice_id: choice.for_choice,
+ input_that_chooses_num: chosenNum,
+ });
+ // TODO: Re-check if this env should be the same as the initial env or a new one.
+ const appliedInput = applyAllInputs(env, state, currentContract, [input]);
+ // TODO: Improve error handling
+ if (typeof appliedInput === "string") throw new Error(appliedInput);
+ return {
+ inputs: [input],
+ environment: env,
+ reducedState: appliedInput.state,
+ reducedContract: appliedInput.continuation,
+ warnings: [...previousWarnings, ...appliedInput.warnings],
+ payments: [...previousPayments, ...appliedInput.payments],
+ };
+ },
+ };
} else {
- }
+ const notify = cse.case;
+ if (!evalObservation(env, state, notify.notify_if)) {
+ return;
+ }
+
+ return {
+ type: "Notify",
- // if (cse.case)
+ applyAction() {
+ const input = decorateInput("input_notify");
+ // TODO: Re-check if this env should be the same as the initial env or a new one.
+ const appliedInput = applyAllInputs(env, state, currentContract, [input]);
+ // TODO: Improve error handling
+ if (typeof appliedInput === "string") throw new Error(appliedInput);
+ return {
+ inputs: [input],
+ environment: env,
+ reducedState: appliedInput.state,
+ reducedContract: appliedInput.continuation,
+ warnings: [...previousWarnings, ...appliedInput.warnings],
+ payments: [...previousPayments, ...appliedInput.payments],
+ };
+ },
+ };
+ }
}
diff --git a/examples/nodejs/src/marlowe-object-flow.ts b/examples/nodejs/src/marlowe-object-flow.ts
index 4c64df66..fe6a4a7c 100644
--- a/examples/nodejs/src/marlowe-object-flow.ts
+++ b/examples/nodejs/src/marlowe-object-flow.ts
@@ -23,6 +23,8 @@ import { Bundle, Label, lovelace } from "@marlowe.io/marlowe-object";
import { input, select } from "@inquirer/prompts";
import { RuntimeLifecycle } from "@marlowe.io/runtime-lifecycle/api";
import { MarloweJSON } from "@marlowe.io/adapter/codec";
+import { ContractDetails } from "@marlowe.io/runtime-rest-client/contract";
+import { getApplicableActions } from "./applicable-inputs.js";
main();
// #region Interactive menu
@@ -129,14 +131,7 @@ async function loadContractMenu(lifecycle: RuntimeLifecycle) {
await contractMenu(lifecycle, contractId(cid));
}
-async function contractMenu(
- lifecycle: RuntimeLifecycle,
- contractId: ContractId
-) {
- console.log("TODO: print contract state");
- const contractDetails = await lifecycle.restClient.getContractById(
- contractId
- );
+async function debugGetNext(lifecycle: RuntimeLifecycle, contractDetails: ContractDetails, contractId: ContractId) {
const now = datetoTimeout(new Date());
if (contractDetails.currentContract._tag === "None") {
@@ -158,6 +153,32 @@ async function contractMenu(
console.log("applicable inputs");
console.log(MarloweJSON.stringify(applicableInputs, null, 2));
+}
+
+async function contractMenu(
+ lifecycle: RuntimeLifecycle,
+ contractId: ContractId
+) {
+ console.log("TODO: print contract state");
+ const contractDetails = await lifecycle.restClient.getContractById(
+ contractId
+ );
+ // await debugGetNext(lifecycle, contractDetails, contractId);
+
+ const applicableActions = await getApplicableActions(lifecycle.restClient, contractId);
+ applicableActions.forEach(action => {
+ console.log("***");
+ console.log(MarloweJSON.stringify(action, null, 2));
+ let result;
+ if (action.type === "Choice") {
+ console.log("automatically choosing", action.choice.choose_between[0].from);
+ result = action.applyAction(action.choice.choose_between[0].from);
+ } else {
+ result = action.applyAction();
+ }
+ console.log("expected results", MarloweJSON.stringify(result, null, 2));
+ })
+
const answer = await select({
message: "Contract menu",
choices: [
diff --git a/packages/language/core/v1/src/inputs.ts b/packages/language/core/v1/src/inputs.ts
index 693d560f..851aaacc 100644
--- a/packages/language/core/v1/src/inputs.ts
+++ b/packages/language/core/v1/src/inputs.ts
@@ -1,5 +1,5 @@
import * as t from "io-ts/lib/index.js";
-import { ContractGuard } from "./contract.js";
+import { Contract, ContractGuard } from "./contract.js";
import {
ChoiceId,
ChoiceIdGuard,
@@ -9,6 +9,7 @@ import {
import { Party, PartyGuard } from "./participants.js";
import { AccountId, AccountIdGuard } from "./payee.js";
import { Token, TokenGuard } from "./token.js";
+import { Deposit } from "./next/index.js";
/**
* TODO: Comment
@@ -115,23 +116,49 @@ export type NormalInput = InputContent;
*/
export const NormalInputGuard = InputContentGuard;
+export interface MerkleizedHashAndContinuation {
+ continuation_hash: BuiltinByteString;
+ merkleized_continuation: Contract;
+}
+
+export const MerkleizedHashAndContinuationGuard: t.Type = t.type({
+ continuation_hash: BuiltinByteStringGuard,
+ merkleized_continuation: ContractGuard,
+});
+
+export type MerkleizedDeposit = IDeposit & MerkleizedHashAndContinuation;
+
+export const MerkleizedDepositGuard: t.Type = t.intersection([
+ IDepositGuard,
+ MerkleizedHashAndContinuationGuard,
+]);
+
+export type MerkleizedChoice = IChoice & MerkleizedHashAndContinuation;
+
+export const MerkleizedChoiceGuard: t.Type = t.intersection([
+ IChoiceGuard,
+ MerkleizedHashAndContinuationGuard,
+]);
+
+// NOTE: Because INotify is serialized as a string, it is invalid to do the &.
+// the type in marlowe-cardano is serialized just as the hash and continuation.
+export type MerkleizedNotify = MerkleizedHashAndContinuation;
+export const MerkleizedNotifyGuard = MerkleizedHashAndContinuationGuard;
+
/**
* TODO: Revisit
* @category Input
*/
-export type MerkleizedInput = t.TypeOf;
+export type MerkleizedInput = MerkleizedDeposit | MerkleizedChoice | MerkleizedNotify;
/**
* TODO: Revisit
* @category Input
*/
-export const MerkleizedInputGuard = t.intersection([
- InputContentGuard,
- t.type({
- continuation_hash: BuiltinByteStringGuard,
- merkleized_continuation: ContractGuard,
- }),
+export const MerkleizedInputGuard =t.union([
+ MerkleizedDepositGuard,
+ MerkleizedChoiceGuard,
+ MerkleizedNotifyGuard,
]);
-
/**
* TODO: Revisit
* @category Input
diff --git a/packages/language/core/v1/src/semantics.ts b/packages/language/core/v1/src/semantics.ts
index 2dac5380..8f0ec5f6 100644
--- a/packages/language/core/v1/src/semantics.ts
+++ b/packages/language/core/v1/src/semantics.ts
@@ -59,7 +59,7 @@ import { Action } from "./actions.js";
import { choiceIdCmp, inBounds } from "./choices.js";
import { Case, Contract, matchContract } from "./contract.js";
import { Environment, TimeInterval } from "./environment.js";
-import { Input, InputContent } from "./inputs.js";
+import { IChoice, IDeposit, Input, InputContent } from "./inputs.js";
import { Party } from "./participants.js";
import { AccountId, matchPayee, Payee } from "./payee.js";
import { Accounts, accountsCmp, MarloweState } from "./state.js";
@@ -109,7 +109,7 @@ export {
TransactionSuccess,
TransactionOutput,
} from "./transaction.js";
-
+export {inBounds};
/**
* The function moneyInAccount returns the number of tokens a particular AccountId has in their account.
* @hidden
@@ -718,6 +718,21 @@ const hashMismatchError = "TEHashMismatch" as const;
type ApplyResult = AppliedResult | ApplyNoMatchError | HashMismatchError;
+
+function inputToInputContent (input: Input): InputContent {
+ if (input === "input_notify") {
+ return "input_notify";
+ }
+ if ("that_deposits" in input) {
+ input
+ return input as IDeposit
+ }
+ if ("input_that_chooses_num" in input) {
+ input
+ return input as IChoice;
+ }
+ return "input_notify"
+}
/**
* @hidden
*/
@@ -731,7 +746,7 @@ function applyCases(
const [headCase, ...tailCases] = cases;
const action = headCase.case;
const cont = getContinuation(input, headCase);
- const result = applyAction(env, state, input, action);
+ const result = applyAction(env, state, inputToInputContent(input), action);
switch (result.type) {
case "AppliedAction":
if (typeof cont === "undefined") {
@@ -752,7 +767,7 @@ function applyCases(
/**
* @hidden
*/
-export function applyInput(
+function applyInput(
env: Environment,
state: MarloweState,
input: Input,
@@ -824,7 +839,7 @@ type ApplyAllResult =
/**
* @hidden
*/
-function applyAllInputs(
+export function applyAllInputs(
env: Environment,
state: MarloweState,
cont: Contract,