From 921cd84dafcaee36d3b61d5210f1ea92004d32d5 Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Thu, 21 Dec 2023 23:05:14 -0300 Subject: [PATCH] wip applicable inputs --- examples/nodejs/src/applicable-inputs.ts | 306 ++++++++++++--------- examples/nodejs/src/marlowe-object-flow.ts | 37 ++- packages/language/core/v1/src/inputs.ts | 45 ++- packages/language/core/v1/src/semantics.ts | 25 +- 4 files changed, 268 insertions(+), 145 deletions(-) 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,