From 7643c0a0998909f080723646b70cb644c42aab33 Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Wed, 13 Dec 2023 14:04:44 -0300 Subject: [PATCH 01/22] wip marlowe-object flow --- examples/nodejs/package-lock.json | 198 +++++++++++++++++++-- examples/nodejs/package.json | 9 +- examples/nodejs/src/marlowe-object-flow.ts | 123 +++++++++++++ examples/nodejs/src/tsconfig.json | 6 + examples/nodejs/src/wallet-flow.ts | 2 +- examples/nodejs/tsconfig.json | 103 ----------- tsconfig.json | 4 +- 7 files changed, 324 insertions(+), 121 deletions(-) create mode 100644 examples/nodejs/src/marlowe-object-flow.ts create mode 100644 examples/nodejs/src/tsconfig.json delete mode 100644 examples/nodejs/tsconfig.json diff --git a/examples/nodejs/package-lock.json b/examples/nodejs/package-lock.json index 6a9e7dc3..0c39a98a 100644 --- a/examples/nodejs/package-lock.json +++ b/examples/nodejs/package-lock.json @@ -12,27 +12,52 @@ "arg": "^5.0.2" }, "devDependencies": { + "ts-node": "^10.9.2", "typescript": "^4.9.5" } }, - "node_modules/@marlowe.io/adapter": { - "version": "0.2.0-beta", - "resolved": "https://registry.npmjs.org/@marlowe.io/adapter/-/adapter-0.2.0-beta.tgz", - "integrity": "sha512-J7jQJyRKdDLQkjw+pTOHPYjzz6FCAE5mDLqx/mpvZqKPlXmilvqXCtkta8ZJVIqqQ6lPbZQMXMbm9JtKBFbDZA==", + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, "dependencies": { - "date-fns": "2.29.3", - "fp-ts": "^2.16.0", - "io-ts": "2.2.20", - "json-bigint": "^1.0.0", - "newtype-ts": "0.3.5" + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, "node_modules/@marlowe.io/language-core-v1": { - "version": "0.2.0-beta", - "resolved": "https://registry.npmjs.org/@marlowe.io/language-core-v1/-/language-core-v1-0.2.0-beta.tgz", - "integrity": "sha512-z0adl0dvPxky8KBQxLjLzGHZ3NuQZHZ2ihV9jxUuAsv2Ei0JG8mxb3zhEx4AflqjizXMAnku8KWBhEfYmlaaCg==", + "version": "0.2.0-alpha-22", + "resolved": "https://registry.npmjs.org/@marlowe.io/language-core-v1/-/language-core-v1-0.2.0-alpha-22.tgz", + "integrity": "sha512-NC0fXyNXcusK1VElOvgpk0M1y+QNvi4W8x1QMej6R/apA/gSdUOXcVA8zUa6QDglVyWieYstYyi5aso1sefqyQ==", "dependencies": { - "@marlowe.io/adapter": "0.2.0-beta", "date-fns": "2.29.3", "fp-ts": "^2.16.0", "io-ts": "2.2.20", @@ -48,6 +73,61 @@ "integrity": "sha512-lHKK8M5CTcpFj2hZDB3wIjb0KAbEOgDmiJGDv1WBRfQgRm/a8/XMEkG/N1iM01xgbUDsPQwi42D+dFo1XPAKew==", "hasInstallScript": true }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", + "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==", + "dev": true, + "peer": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -61,6 +141,12 @@ "node": "*" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/date-fns": { "version": "2.29.3", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", @@ -73,6 +159,15 @@ "url": "https://opencollective.com/date-fns" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/fp-ts": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.1.tgz", @@ -117,6 +212,12 @@ "io-ts": "^2.2.16" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "node_modules/monocle-ts": { "version": "2.3.13", "resolved": "https://registry.npmjs.org/monocle-ts/-/monocle-ts-2.3.13.tgz", @@ -135,6 +236,55 @@ "monocle-ts": "^2.0.0" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -147,6 +297,28 @@ "engines": { "node": ">=4.2.0" } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "peer": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } } } } diff --git a/examples/nodejs/package.json b/examples/nodejs/package.json index 70810599..c08aeb2a 100644 --- a/examples/nodejs/package.json +++ b/examples/nodejs/package.json @@ -3,12 +3,15 @@ "version": "0.0.1", "private": true, "scripts": { - "build": "tsc", - "wallet-flow": "node dist/wallet-flow.js", - "escrow": "node dist/escrow-flow.js" + "build": "tsc --build src", + "clean": "tsc --build --clean && shx rm -rf dist", + "wallet-flow": "ts-node-esm src/wallet-flow.ts", + "escrow": "ts-node-esm src/escrow-flow.ts", + "marlowe-object-flow": "ts-node-esm src/marlowe-object-flow.ts" }, "type": "module", "devDependencies": { + "ts-node": "^10.9.2", "typescript": "^4.9.5" }, "dependencies": { diff --git a/examples/nodejs/src/marlowe-object-flow.ts b/examples/nodejs/src/marlowe-object-flow.ts new file mode 100644 index 00000000..8c18f4c3 --- /dev/null +++ b/examples/nodejs/src/marlowe-object-flow.ts @@ -0,0 +1,123 @@ +/** + * This example shows how to work with the marlowe-object package, which is needed when we + * want to create large contracts through the use of Merkleization. + * + * The script is a command line tool that makes a delay payment to a given address. + */ +import arg from "arg"; + +import { mkLucidWallet } from "@marlowe.io/wallet"; +import { mkRuntimeLifecycle } from "@marlowe.io/runtime-lifecycle"; +import { Lucid, Blockfrost } from "lucid-cardano"; +import { readConfig } from "./config.js"; +import { datetoTimeout } from "@marlowe.io/language-core-v1"; +import { addressBech32 } from "@marlowe.io/runtime-core"; + +main(); +type CliArgs = ReturnType; + +function parseCliArgs() { + const args = arg({ + "--help": Boolean, + "--pay-to": String, + "--amount": Number, + "--deposit-deadline": String, + "--release-deadline": String, + "-a": "--amount", + }); + + function printHelp(exitStatus: number): never { + console.log( + "Usage: npm run marlowe-object-flow -- " + ); + console.log(""); + console.log("Example:"); + console.log( + " npm run marlowe-object-flow -- --pay-to addr1_af33.... -a 10000000 --deposit-deadline 2024-01-01 --release-deadline 2024-01-02" + ); + console.log("Options:"); + console.log(" --help: Print this message"); + console.log(" --pay-to: The address of the payee"); + console.log(" --amount: The amount of lovelace to pay"); + console.log(" --deposit-deadline: When the payment must be deposited"); + console.log( + " --release-deadline: When the payment is released from the contract to the payee" + ); + console.log(""); + console.log( + "All dates must be in a format that is parsable by the Date constructor" + ); + console.log(""); + process.exit(exitStatus); + } + + function badCliOptions(message: string) { + console.error("********** ERROR **********"); + console.error(message); + console.error(""); + console.error(""); + console.error(""); + return printHelp(1); + } + + if (args["--help"]) { + printHelp(0); + } + + const payTo = + args["--pay-to"] ?? + badCliOptions("You must specify the address of the payee"); + const amount = + args["--amount"] ?? + badCliOptions("You must specify the amount of lovelace to pay"); + const depositDeadlineStr = + args["--deposit-deadline"] ?? + badCliOptions("You must specify the deposit deadline"); + const releaseDeadlineStr = + args["--release-deadline"] ?? + badCliOptions("You must specify the release deadline"); + + const depositDeadline = new Date(depositDeadlineStr); + const releaseDeadline = new Date(releaseDeadlineStr); + + // Check if depositDeadline and releaseDeadline are valid dates and both are in the future + if ( + isNaN(depositDeadline.getTime()) || + isNaN(releaseDeadline.getTime()) || + depositDeadline <= new Date() || + releaseDeadline <= new Date() + ) { + badCliOptions( + "Invalid deposit deadline or release deadline. Both must be valid dates in the future." + ); + } + return { + payTo, + amount, + depositDeadline, + releaseDeadline, + }; +} + +async function main() { + const args = parseCliArgs(); + const config = await readConfig(); + const lucid = await Lucid.new( + new Blockfrost(config.blockfrostUrl, config.blockfrostProjectId), + config.network + ); + lucid.selectWalletFromSeed(config.seedPhrase); + + const runtimeURL = config.runtimeURL; + + const wallet = mkLucidWallet(lucid); + + const lifecycle = mkRuntimeLifecycle({ + runtimeURL, + wallet, + }); + const walletAddress = await wallet.getChangeAddress(); + + console.log(`Making a delayed payment from ${walletAddress} to ${args.payTo} for ${args.amount} lovelaces`); + console.log(`The payment must be deposited by ${args.depositDeadline} and will be released to ${args.payTo} by ${args.releaseDeadline}`); +} diff --git a/examples/nodejs/src/tsconfig.json b/examples/nodejs/src/tsconfig.json new file mode 100644 index 00000000..883f4cd4 --- /dev/null +++ b/examples/nodejs/src/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../tsconfig-base.json", + "compilerOptions": { + "outDir": "../dist" + } +} diff --git a/examples/nodejs/src/wallet-flow.ts b/examples/nodejs/src/wallet-flow.ts index f2d2b430..0090a6f0 100644 --- a/examples/nodejs/src/wallet-flow.ts +++ b/examples/nodejs/src/wallet-flow.ts @@ -49,4 +49,4 @@ async function main() { log("Wallet flow done 🎉"); } -await main(); +main(); diff --git a/examples/nodejs/tsconfig.json b/examples/nodejs/tsconfig.json deleted file mode 100644 index c5571c82..00000000 --- a/examples/nodejs/tsconfig.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "Node16" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ - // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist" /* Specify an output folder for all emitted files. */, - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } -} diff --git a/tsconfig.json b/tsconfig.json index a6cd9002..eff7641f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,8 @@ { "path": "./packages/runtime/core/src" }, { "path": "./packages/runtime/lifecycle/src" }, { "path": "./packages/token-metadata-client/src" }, - { "path": "./packages/marlowe-object/src" } + { "path": "./packages/marlowe-object/src" }, + { "path": "./examples/nodejs/src" }, + ] } From 50e07bd0efdb2f91b2f3a8e74fdc6ef04e4f21d5 Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Wed, 13 Dec 2023 18:12:22 -0300 Subject: [PATCH 02/22] wip marlowe-object flow --- examples/nodejs/package-lock.json | 636 ++++++++++++++++++++- examples/nodejs/package.json | 1 + examples/nodejs/src/marlowe-object-flow.ts | 186 +++++- 3 files changed, 811 insertions(+), 12 deletions(-) diff --git a/examples/nodejs/package-lock.json b/examples/nodejs/package-lock.json index 0c39a98a..2cb10d5f 100644 --- a/examples/nodejs/package-lock.json +++ b/examples/nodejs/package-lock.json @@ -8,6 +8,7 @@ "name": "ts-sdk-node-example", "version": "0.0.1", "dependencies": { + "@inquirer/prompts": "^3.3.0", "@marlowe.io/language-core-v1": "^0.2.0-alpha-22", "arg": "^5.0.2" }, @@ -28,6 +29,402 @@ "node": ">=12" } }, + "node_modules/@inquirer/checkbox": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-1.5.0.tgz", + "integrity": "sha512-3cKJkW1vIZAs4NaS0reFsnpAjP0azffYII4I2R7PTI7ZTMg5Y1at4vzXccOH3762b2c2L4drBhpJpf9uiaGNxA==", + "dependencies": { + "@inquirer/core": "^5.1.1", + "@inquirer/type": "^1.1.5", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "figures": "^3.2.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@inquirer/checkbox/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@inquirer/checkbox/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@inquirer/checkbox/node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@inquirer/confirm": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-2.0.15.tgz", + "integrity": "sha512-hj8Q/z7sQXsF0DSpLQZVDhWYGN6KLM/gNjjqGkpKwBzljbQofGjn0ueHADy4HUY+OqDHmXuwk/bY+tZyIuuB0w==", + "dependencies": { + "@inquirer/core": "^5.1.1", + "@inquirer/type": "^1.1.5", + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@inquirer/confirm/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@inquirer/core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-5.1.1.tgz", + "integrity": "sha512-IuJyZQUg75+L5AmopgnzxYrgcU6PJKL0hoIs332G1Gv55CnmZrhG6BzNOeZ5sOsTi1YCGOopw4rYICv74ejMFg==", + "dependencies": { + "@inquirer/type": "^1.1.5", + "@types/mute-stream": "^0.0.4", + "@types/node": "^20.9.0", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "cli-spinners": "^2.9.1", + "cli-width": "^4.1.0", + "figures": "^3.2.0", + "mute-stream": "^1.0.0", + "run-async": "^3.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@inquirer/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@inquirer/core/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@inquirer/core/node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/editor": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-1.2.13.tgz", + "integrity": "sha512-gBxjqt0B9GLN0j6M/tkEcmcIvB2fo9Cw0f5NRqDTkYyB9AaCzj7qvgG0onQ3GVPbMyMbbP4tWYxrBOaOdKpzNA==", + "dependencies": { + "@inquirer/core": "^5.1.1", + "@inquirer/type": "^1.1.5", + "chalk": "^4.1.2", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@inquirer/editor/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@inquirer/expand": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-1.1.14.tgz", + "integrity": "sha512-yS6fJ8jZYAsxdxuw2c8XTFMTvMR1NxZAw3LxDaFnqh7BZ++wTQ6rSp/2gGJhMacdZ85osb+tHxjVgx7F+ilv5g==", + "dependencies": { + "@inquirer/core": "^5.1.1", + "@inquirer/type": "^1.1.5", + "chalk": "^4.1.2", + "figures": "^3.2.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@inquirer/expand/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@inquirer/expand/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@inquirer/expand/node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@inquirer/input": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-1.2.14.tgz", + "integrity": "sha512-tISLGpUKXixIQue7jypNEShrdzJoLvEvZOJ4QRsw5XTfrIYfoWFqAjMQLerGs9CzR86yAI89JR6snHmKwnNddw==", + "dependencies": { + "@inquirer/core": "^5.1.1", + "@inquirer/type": "^1.1.5", + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@inquirer/input/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@inquirer/password": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-1.1.14.tgz", + "integrity": "sha512-vL2BFxfMo8EvuGuZYlryiyAB3XsgtbxOcFs4H9WI9szAS/VZCAwdVqs8rqEeaAf/GV/eZOghIOYxvD91IsRWSg==", + "dependencies": { + "@inquirer/input": "^1.2.14", + "@inquirer/type": "^1.1.5", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@inquirer/password/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@inquirer/prompts": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-3.3.0.tgz", + "integrity": "sha512-BBCqdSnhNs+WziSIo4f/RNDu6HAj4R/Q5nMgJb5MNPFX8sJGCvj9BoALdmR0HTWXyDS7TO8euKj6W6vtqCQG7A==", + "dependencies": { + "@inquirer/checkbox": "^1.5.0", + "@inquirer/confirm": "^2.0.15", + "@inquirer/core": "^5.1.1", + "@inquirer/editor": "^1.2.13", + "@inquirer/expand": "^1.1.14", + "@inquirer/input": "^1.2.14", + "@inquirer/password": "^1.1.14", + "@inquirer/rawlist": "^1.2.14", + "@inquirer/select": "^1.3.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-1.2.14.tgz", + "integrity": "sha512-xIYmDpYgfz2XGCKubSDLKEvadkIZAKbehHdWF082AyC2I4eHK44RUfXaoOAqnbqItZq4KHXS6jDJ78F2BmQvxg==", + "dependencies": { + "@inquirer/core": "^5.1.1", + "@inquirer/type": "^1.1.5", + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@inquirer/rawlist/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@inquirer/select": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-1.3.1.tgz", + "integrity": "sha512-EgOPHv7XOHEqiBwBJTyiMg9r57ySyW4oyYCumGp+pGyOaXQaLb2kTnccWI6NFd9HSi5kDJhF7YjA+3RfMQJ2JQ==", + "dependencies": { + "@inquirer/core": "^5.1.1", + "@inquirer/type": "^1.1.5", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "figures": "^3.2.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@inquirer/select/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@inquirer/select/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@inquirer/select/node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@inquirer/type": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.1.5.tgz", + "integrity": "sha512-wmwHvHozpPo4IZkkNtbYenem/0wnfI6hvOcGKmPEa0DwuaH5XUQzFqy6OpEpjEegZMhYIk8HDYITI16BPLtrRA==", + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", @@ -97,16 +494,27 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "20.10.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==", - "dev": true, - "peer": true, "dependencies": { "undici-types": "~5.26.4" } }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==" + }, "node_modules/acorn": { "version": "8.11.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", @@ -128,6 +536,42 @@ "node": ">=0.4.0" } }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -141,6 +585,46 @@ "node": "*" } }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -168,11 +652,48 @@ "node": ">=0.3.1" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/fp-ts": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.1.tgz", "integrity": "sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA==" }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/io-ts": { "version": "2.2.20", "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.20.tgz", @@ -192,6 +713,14 @@ "newtype-ts": "^0.3.2" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, "node_modules/json-bigint": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", @@ -227,6 +756,14 @@ "fp-ts": "^2.5.0" } }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/newtype-ts": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/newtype-ts/-/newtype-ts-0.3.5.tgz", @@ -236,6 +773,73 @@ "monocle-ts": "^2.0.0" } }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -285,6 +889,17 @@ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -301,9 +916,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, - "peer": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", @@ -311,6 +924,19 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/examples/nodejs/package.json b/examples/nodejs/package.json index c08aeb2a..2f017d98 100644 --- a/examples/nodejs/package.json +++ b/examples/nodejs/package.json @@ -15,6 +15,7 @@ "typescript": "^4.9.5" }, "dependencies": { + "@inquirer/prompts": "^3.3.0", "@marlowe.io/language-core-v1": "^0.2.0-alpha-22", "arg": "^5.0.2" } diff --git a/examples/nodejs/src/marlowe-object-flow.ts b/examples/nodejs/src/marlowe-object-flow.ts index 8c18f4c3..4822aa32 100644 --- a/examples/nodejs/src/marlowe-object-flow.ts +++ b/examples/nodejs/src/marlowe-object-flow.ts @@ -8,12 +8,129 @@ import arg from "arg"; import { mkLucidWallet } from "@marlowe.io/wallet"; import { mkRuntimeLifecycle } from "@marlowe.io/runtime-lifecycle"; -import { Lucid, Blockfrost } from "lucid-cardano"; +import { Lucid, Blockfrost, C } from "lucid-cardano"; import { readConfig } from "./config.js"; import { datetoTimeout } from "@marlowe.io/language-core-v1"; -import { addressBech32 } from "@marlowe.io/runtime-core"; - +import { addressBech32, ContractId } from "@marlowe.io/runtime-core"; +import { Address } from "@marlowe.io/language-core-v1"; +import { Bundle, Label, lovelace } from "@marlowe.io/marlowe-object"; +import { input, select } from "@inquirer/prompts"; main(); + +function bech32Validator(value: string) { + try { + C.Address.from_bech32(value); + return true; + } catch (e) { + return "Invalid address"; + } +} +function positiveBigIntValidator (value: string) { + try { + if (BigInt(value) > 0) { + return true; + } else { + return "The amount must be greater than 0"; + } + } catch (e) { + return "The amount must be a number"; + } +} + +function dateInFutureValidator (value: string) { + const d = new Date(value); + if (isNaN(d.getTime())) { + return "Invalid date"; + } + if (d <= new Date()) { + return "The date must be in the future"; + } + return true; +} + +async function createContractMenu() { + const payee = await input({ + message: "Enter the payee address", + validate: bech32Validator, + }); + console.log(payee); + const amountStr = await input({ + message: "Enter the payment amount in lovelaces", + validate: positiveBigIntValidator, + }); + + const amount = BigInt(amountStr); + console.log(amount); + + const depositDeadlineStr = await input({ + message: "Enter the deposit deadline", + validate: dateInFutureValidator, + }); + const depositDeadline = new Date(depositDeadlineStr); + console.log(depositDeadline); + + const releaseDeadlineStr = await input({ + message: "Enter the release deadline", + validate: dateInFutureValidator, + }); + const releaseDeadline = new Date(releaseDeadlineStr); + console.log(releaseDeadline); + await contractMenu(); +} + +async function loadContractMenu() { + const answer = await input({ + message: "Enter the contractId", + }); + console.log(answer); + await contractMenu(); +} + +// async function contractMenu(contractId: ContractId) { +async function contractMenu() { + console.log("TODO: print contract state"); + const answer = await select({ + message: "Contract menu", + choices: [ + { name: "Re-check contract state", value: "check-state" }, + { name: "Deposit", value: "deposit" }, + { name: "Release funds", value: "release" }, + { name: "Return to main menu", value: "return" }, + ], + }); +} + +async function mainLoop() { + try { + while (true) { + const action = await select({ + message: "Main menu", + choices: [ + { name: "Create a contract", value: "create" }, + { name: "Load contract", value: "load" }, + { name: "Exit", value: "exit" }, + ], + }); + switch (action) { + case "create": + await createContractMenu(); + break; + case "load": + await loadContractMenu(); + break; + case "exit": + process.exit(0); + } + } + } catch (e) { + if (e instanceof Error && e.message.includes("closed the prompt")) { + process.exit(0); + } else { + throw e; + } + } +} + type CliArgs = ReturnType; function parseCliArgs() { @@ -99,8 +216,59 @@ function parseCliArgs() { }; } +interface DelayPaymentSchema { + payFrom: Address; + payTo: Address; + amount: bigint; + depositDeadline: Date; + releaseDeadline: Date; +} +// TODO: move to marlowe-object +type ContractBundle = { + main: Label; + bundle: Bundle; +}; + +function mkDelayPayment(schema: DelayPaymentSchema): ContractBundle { + return { + main: "initial-deposit", + bundle: [ + { + label: "release-funds", + type: "contract", + value: { + when: [], + timeout: datetoTimeout(schema.releaseDeadline), + timeout_continuation: "close", + }, + }, + { + label: "initial-deposit", + type: "contract", + value: { + when: [ + { + case: { + party: schema.payFrom, + deposits: schema.amount, + of_token: lovelace, + into_account: schema.payTo, + }, + then: { + ref: "release-funds", + }, + }, + ], + timeout: datetoTimeout(schema.depositDeadline), + timeout_continuation: "close", + }, + }, + ], + }; +} + async function main() { - const args = parseCliArgs(); + // const args = parseCliArgs(); const config = await readConfig(); const lucid = await Lucid.new( new Blockfrost(config.blockfrostUrl, config.blockfrostProjectId), @@ -117,7 +285,11 @@ async function main() { wallet, }); const walletAddress = await wallet.getChangeAddress(); - - console.log(`Making a delayed payment from ${walletAddress} to ${args.payTo} for ${args.amount} lovelaces`); - console.log(`The payment must be deposited by ${args.depositDeadline} and will be released to ${args.payTo} by ${args.releaseDeadline}`); + await mainLoop(); + // console.log( + // `Making a delayed payment from ${walletAddress} to ${args.payTo} for ${args.amount} lovelaces` + // ); + // console.log( + // `The payment must be deposited by ${args.depositDeadline} and will be released to ${args.payTo} by ${args.releaseDeadline}` + // ); } From 83cf591ef80b48722c4c178d4b969896b1fc36f8 Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Thu, 14 Dec 2023 11:28:59 -0300 Subject: [PATCH 03/22] wip Add delay payment creation --- examples/nodejs/src/marlowe-object-flow.ts | 216 +++++++++--------- packages/runtime/lifecycle/src/api.ts | 3 +- .../runtime/lifecycle/src/generic/runtime.ts | 1 + 3 files changed, 112 insertions(+), 108 deletions(-) diff --git a/examples/nodejs/src/marlowe-object-flow.ts b/examples/nodejs/src/marlowe-object-flow.ts index 4822aa32..c6f5b817 100644 --- a/examples/nodejs/src/marlowe-object-flow.ts +++ b/examples/nodejs/src/marlowe-object-flow.ts @@ -4,19 +4,46 @@ * * The script is a command line tool that makes a delay payment to a given address. */ -import arg from "arg"; - -import { mkLucidWallet } from "@marlowe.io/wallet"; +import { mkLucidWallet, WalletAPI } from "@marlowe.io/wallet"; import { mkRuntimeLifecycle } from "@marlowe.io/runtime-lifecycle"; import { Lucid, Blockfrost, C } from "lucid-cardano"; import { readConfig } from "./config.js"; import { datetoTimeout } from "@marlowe.io/language-core-v1"; -import { addressBech32, ContractId } from "@marlowe.io/runtime-core"; +import { + addressBech32, + ContractId, + contractIdToTxId, + Tags, + transactionWitnessSetTextEnvelope, + TxId, + unAddressBech32, +} from "@marlowe.io/runtime-core"; import { Address } from "@marlowe.io/language-core-v1"; import { Bundle, Label, lovelace } from "@marlowe.io/marlowe-object"; import { input, select } from "@inquirer/prompts"; +import { RuntimeLifecycle } from "@marlowe.io/runtime-lifecycle/api"; main(); +// #region Interactive menu +async function waitIndicator(wallet: WalletAPI, txId: TxId) { + process.stdout.write("Waiting for the transaction to be confirmed..."); + let done = false; + function writeDot(): Promise { + process.stdout.write("."); + return new Promise((resolve) => setTimeout(resolve, 1000)).then(() => { + if (!done) { + return writeDot(); + } + }); + } + + await Promise.all([ + wallet.waitConfirmation(txId).then(() => (done = true)), + writeDot(), + ]); + process.stdout.write("\n"); +} + function bech32Validator(value: string) { try { C.Address.from_bech32(value); @@ -25,7 +52,7 @@ function bech32Validator(value: string) { return "Invalid address"; } } -function positiveBigIntValidator (value: string) { +function positiveBigIntValidator(value: string) { try { if (BigInt(value) > 0) { return true; @@ -37,7 +64,7 @@ function positiveBigIntValidator (value: string) { } } -function dateInFutureValidator (value: string) { +function dateInFutureValidator(value: string) { const d = new Date(value); if (isNaN(d.getTime())) { return "Invalid date"; @@ -48,33 +75,49 @@ function dateInFutureValidator (value: string) { return true; } -async function createContractMenu() { +async function createContractMenu(lifecycle: RuntimeLifecycle) { const payee = await input({ message: "Enter the payee address", validate: bech32Validator, }); - console.log(payee); const amountStr = await input({ message: "Enter the payment amount in lovelaces", validate: positiveBigIntValidator, }); const amount = BigInt(amountStr); - console.log(amount); const depositDeadlineStr = await input({ message: "Enter the deposit deadline", validate: dateInFutureValidator, }); const depositDeadline = new Date(depositDeadlineStr); - console.log(depositDeadline); const releaseDeadlineStr = await input({ message: "Enter the release deadline", validate: dateInFutureValidator, }); const releaseDeadline = new Date(releaseDeadlineStr); - console.log(releaseDeadline); + + const walletAddress = await lifecycle.wallet.getChangeAddress(); + console.log( + `Making a delayed payment from ${walletAddress} to ${payee} for ${amount} lovelaces` + ); + console.log( + `The payment must be deposited by ${depositDeadline} and will be released to ${payee} by ${releaseDeadline}` + ); + + const [contractId, txId] = await createContract(lifecycle, { + payFrom: { address: unAddressBech32(walletAddress) }, + payTo: { address: payee }, + amount, + depositDeadline, + releaseDeadline, + }); + + console.log(`Contract created with id ${contractId}`); + await waitIndicator(lifecycle.wallet, txId); + await contractMenu(); } @@ -100,7 +143,7 @@ async function contractMenu() { }); } -async function mainLoop() { +async function mainLoop(lifecycle: RuntimeLifecycle) { try { while (true) { const action = await select({ @@ -113,7 +156,7 @@ async function mainLoop() { }); switch (action) { case "create": - await createContractMenu(); + await createContractMenu(lifecycle); break; case "load": await loadContractMenu(); @@ -130,92 +173,9 @@ async function mainLoop() { } } } +// #endregion -type CliArgs = ReturnType; - -function parseCliArgs() { - const args = arg({ - "--help": Boolean, - "--pay-to": String, - "--amount": Number, - "--deposit-deadline": String, - "--release-deadline": String, - "-a": "--amount", - }); - - function printHelp(exitStatus: number): never { - console.log( - "Usage: npm run marlowe-object-flow -- " - ); - console.log(""); - console.log("Example:"); - console.log( - " npm run marlowe-object-flow -- --pay-to addr1_af33.... -a 10000000 --deposit-deadline 2024-01-01 --release-deadline 2024-01-02" - ); - console.log("Options:"); - console.log(" --help: Print this message"); - console.log(" --pay-to: The address of the payee"); - console.log(" --amount: The amount of lovelace to pay"); - console.log(" --deposit-deadline: When the payment must be deposited"); - console.log( - " --release-deadline: When the payment is released from the contract to the payee" - ); - console.log(""); - console.log( - "All dates must be in a format that is parsable by the Date constructor" - ); - console.log(""); - process.exit(exitStatus); - } - - function badCliOptions(message: string) { - console.error("********** ERROR **********"); - console.error(message); - console.error(""); - console.error(""); - console.error(""); - return printHelp(1); - } - - if (args["--help"]) { - printHelp(0); - } - - const payTo = - args["--pay-to"] ?? - badCliOptions("You must specify the address of the payee"); - const amount = - args["--amount"] ?? - badCliOptions("You must specify the amount of lovelace to pay"); - const depositDeadlineStr = - args["--deposit-deadline"] ?? - badCliOptions("You must specify the deposit deadline"); - const releaseDeadlineStr = - args["--release-deadline"] ?? - badCliOptions("You must specify the release deadline"); - - const depositDeadline = new Date(depositDeadlineStr); - const releaseDeadline = new Date(releaseDeadlineStr); - - // Check if depositDeadline and releaseDeadline are valid dates and both are in the future - if ( - isNaN(depositDeadline.getTime()) || - isNaN(releaseDeadline.getTime()) || - depositDeadline <= new Date() || - releaseDeadline <= new Date() - ) { - badCliOptions( - "Invalid deposit deadline or release deadline. Both must be valid dates in the future." - ); - } - return { - payTo, - amount, - depositDeadline, - releaseDeadline, - }; -} - +// #region Marlowe specifics interface DelayPaymentSchema { payFrom: Address; payTo: Address; @@ -229,6 +189,55 @@ type ContractBundle = { bundle: Bundle; }; +const splitAddress = ({ address }: Address) => { + const halfLength = Math.floor(address.length / 2); + const s1 = address.substring(0, halfLength); + const s2 = address.substring(halfLength); + return [s1, s2]; +}; + +const mkDelayPaymentTags = (schema: DelayPaymentSchema) => { + const tag = "DELAY_PYMNT-1"; + const tags = {} as Tags; + + tags[`${tag}-from-0`] = splitAddress(schema.payFrom)[0]; + tags[`${tag}-from-1`] = splitAddress(schema.payFrom)[1]; + tags[`${tag}-to-0`] = splitAddress(schema.payTo)[0]; + tags[`${tag}-to-1`] = splitAddress(schema.payTo)[1]; + tags[`${tag}-amount`] = schema.amount; + tags[`${tag}-deposit`] = schema.depositDeadline; + tags[`${tag}-release`] = schema.releaseDeadline; + return tags; +}; +async function createContract( + lifecycle: RuntimeLifecycle, + schema: DelayPaymentSchema +): Promise<[ContractId, TxId]> { + const contractBundle = mkDelayPayment(schema); + const tags = mkDelayPaymentTags(schema); + // TODO: create a new function in lifecycle `createContractFromBundle` + const contractSources = await lifecycle.restClient.createContractSources( + contractBundle.main, + contractBundle.bundle + ); + const walletAddress = await lifecycle.wallet.getChangeAddress(); + const unsignedTx = await lifecycle.restClient.buildCreateContractTx({ + sourceId: contractSources.contractSourceId, + tags, + changeAddress: walletAddress, + minimumLovelaceUTxODeposit: 3_000_000, + version: "v1", + }); + const signedCborHex = await lifecycle.wallet.signTx(unsignedTx.tx.cborHex); + await lifecycle.restClient.submitContract( + unsignedTx.contractId, + transactionWitnessSetTextEnvelope(signedCborHex) + ); + const txId = contractIdToTxId(unsignedTx.contractId); + return [unsignedTx.contractId, txId]; + //---------------- +} + function mkDelayPayment(schema: DelayPaymentSchema): ContractBundle { return { main: "initial-deposit", @@ -268,7 +277,6 @@ function mkDelayPayment(schema: DelayPaymentSchema): ContractBundle { } async function main() { - // const args = parseCliArgs(); const config = await readConfig(); const lucid = await Lucid.new( new Blockfrost(config.blockfrostUrl, config.blockfrostProjectId), @@ -284,12 +292,6 @@ async function main() { runtimeURL, wallet, }); - const walletAddress = await wallet.getChangeAddress(); - await mainLoop(); - // console.log( - // `Making a delayed payment from ${walletAddress} to ${args.payTo} for ${args.amount} lovelaces` - // ); - // console.log( - // `The payment must be deposited by ${args.depositDeadline} and will be released to ${args.payTo} by ${args.releaseDeadline}` - // ); + await mainLoop(lifecycle); } +// #endregion diff --git a/packages/runtime/lifecycle/src/api.ts b/packages/runtime/lifecycle/src/api.ts index c87fc4ed..2a1e44d1 100644 --- a/packages/runtime/lifecycle/src/api.ts +++ b/packages/runtime/lifecycle/src/api.ts @@ -10,7 +10,7 @@ import { Tags, TxId, } from "@marlowe.io/runtime-core"; -import { RestDI } from "@marlowe.io/runtime-rest-client"; +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 { @@ -23,6 +23,7 @@ import { Next } from "@marlowe.io/language-core-v1/next"; export type RuntimeLifecycle = { wallet: WalletAPI; + restClient: RestClient; contracts: ContractsAPI; payouts: PayoutsAPI; }; diff --git a/packages/runtime/lifecycle/src/generic/runtime.ts b/packages/runtime/lifecycle/src/generic/runtime.ts index 990e3b94..3f7d93e4 100644 --- a/packages/runtime/lifecycle/src/generic/runtime.ts +++ b/packages/runtime/lifecycle/src/generic/runtime.ts @@ -13,6 +13,7 @@ export function mkRuntimeLifecycle( ): RuntimeLifecycle { return { wallet: wallet, + restClient, contracts: mkContractLifecycle(wallet, deprecatedRestAPI, restClient), payouts: mkPayoutLifecycle(wallet, deprecatedRestAPI, restClient), }; From 304bd10446e5142c212df6e8a92a79ebd579b0b1 Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Fri, 15 Dec 2023 13:42:50 -0300 Subject: [PATCH 04/22] wip apply merkleized inputs --- examples/nodejs/package-lock.json | 103 --------------------- examples/nodejs/package.json | 1 - examples/nodejs/src/marlowe-object-flow.ts | 45 ++++++--- packages/language/core/v1/src/contract.ts | 32 +++++++ packages/language/core/v1/src/index.ts | 1 + 5 files changed, 67 insertions(+), 115 deletions(-) diff --git a/examples/nodejs/package-lock.json b/examples/nodejs/package-lock.json index 2cb10d5f..ce29a126 100644 --- a/examples/nodejs/package-lock.json +++ b/examples/nodejs/package-lock.json @@ -9,7 +9,6 @@ "version": "0.0.1", "dependencies": { "@inquirer/prompts": "^3.3.0", - "@marlowe.io/language-core-v1": "^0.2.0-alpha-22", "arg": "^5.0.2" }, "devDependencies": { @@ -450,26 +449,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@marlowe.io/language-core-v1": { - "version": "0.2.0-alpha-22", - "resolved": "https://registry.npmjs.org/@marlowe.io/language-core-v1/-/language-core-v1-0.2.0-alpha-22.tgz", - "integrity": "sha512-NC0fXyNXcusK1VElOvgpk0M1y+QNvi4W8x1QMej6R/apA/gSdUOXcVA8zUa6QDglVyWieYstYyi5aso1sefqyQ==", - "dependencies": { - "date-fns": "2.29.3", - "fp-ts": "^2.16.0", - "io-ts": "2.2.20", - "io-ts-types": "0.5.19", - "json-bigint": "^1.0.0", - "jsonbigint-io-ts-reporters": "2.0.1", - "newtype-ts": "0.3.5" - } - }, - "node_modules/@scarf/scarf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.3.0.tgz", - "integrity": "sha512-lHKK8M5CTcpFj2hZDB3wIjb0KAbEOgDmiJGDv1WBRfQgRm/a8/XMEkG/N1iM01xgbUDsPQwi42D+dFo1XPAKew==", - "hasInstallScript": true - }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -577,14 +556,6 @@ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" }, - "node_modules/bignumber.js": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", - "engines": { - "node": "*" - } - }, "node_modules/chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -631,18 +602,6 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, - "node_modules/date-fns": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", - "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -670,11 +629,6 @@ "node": ">=4" } }, - "node_modules/fp-ts": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.1.tgz", - "integrity": "sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA==" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -694,25 +648,6 @@ "node": ">=0.10.0" } }, - "node_modules/io-ts": { - "version": "2.2.20", - "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.20.tgz", - "integrity": "sha512-Rq2BsYmtwS5vVttie4rqrOCIfHCS9TgpRLFpKQCM1wZBBRY9nWVGmEvm2FnDbSE2un1UE39DvFpTR5UL47YDcA==", - "peerDependencies": { - "fp-ts": "^2.5.0" - } - }, - "node_modules/io-ts-types": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/io-ts-types/-/io-ts-types-0.5.19.tgz", - "integrity": "sha512-kQOYYDZG5vKre+INIDZbLeDJe+oM+4zLpUkjXyTMyUfoCpjJNyi29ZLkuEAwcPufaYo3yu/BsemZtbdD+NtRfQ==", - "peerDependencies": { - "fp-ts": "^2.0.0", - "io-ts": "^2.0.0", - "monocle-ts": "^2.0.0", - "newtype-ts": "^0.3.2" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -721,41 +656,12 @@ "node": ">=8" } }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, - "node_modules/jsonbigint-io-ts-reporters": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jsonbigint-io-ts-reporters/-/jsonbigint-io-ts-reporters-2.0.1.tgz", - "integrity": "sha512-Sk8PAR1l/nSGa1h26VbxIYpcOHONcQsS4i1nuHW7xkG+sqh6j4YhII8tU3PiimNBGK7LTlmL4E6wNth0hyayew==", - "dependencies": { - "@scarf/scarf": "^1.1.1" - }, - "peerDependencies": { - "fp-ts": "^2.10.5", - "io-ts": "^2.2.16" - } - }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, - "node_modules/monocle-ts": { - "version": "2.3.13", - "resolved": "https://registry.npmjs.org/monocle-ts/-/monocle-ts-2.3.13.tgz", - "integrity": "sha512-D5Ygd3oulEoAm3KuGO0eeJIrhFf1jlQIoEVV2DYsZUMz42j4tGxgct97Aq68+F8w4w4geEnwFa8HayTS/7lpKQ==", - "peer": true, - "peerDependencies": { - "fp-ts": "^2.5.0" - } - }, "node_modules/mute-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", @@ -764,15 +670,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/newtype-ts": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/newtype-ts/-/newtype-ts-0.3.5.tgz", - "integrity": "sha512-v83UEQMlVR75yf1OUdoSFssjitxzjZlqBAjiGQ4WJaML8Jdc68LJ+BaSAXUmKY4bNzp7hygkKLYTsDi14PxI2g==", - "peerDependencies": { - "fp-ts": "^2.0.0", - "monocle-ts": "^2.0.0" - } - }, "node_modules/os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", diff --git a/examples/nodejs/package.json b/examples/nodejs/package.json index 2f017d98..91167ead 100644 --- a/examples/nodejs/package.json +++ b/examples/nodejs/package.json @@ -16,7 +16,6 @@ }, "dependencies": { "@inquirer/prompts": "^3.3.0", - "@marlowe.io/language-core-v1": "^0.2.0-alpha-22", "arg": "^5.0.2" } } diff --git a/examples/nodejs/src/marlowe-object-flow.ts b/examples/nodejs/src/marlowe-object-flow.ts index c6f5b817..0b949268 100644 --- a/examples/nodejs/src/marlowe-object-flow.ts +++ b/examples/nodejs/src/marlowe-object-flow.ts @@ -8,15 +8,15 @@ import { mkLucidWallet, WalletAPI } from "@marlowe.io/wallet"; import { mkRuntimeLifecycle } from "@marlowe.io/runtime-lifecycle"; import { Lucid, Blockfrost, C } from "lucid-cardano"; import { readConfig } from "./config.js"; -import { datetoTimeout } from "@marlowe.io/language-core-v1"; +import { datetoTimeout, getNextTimeout, Timeout } from "@marlowe.io/language-core-v1"; import { addressBech32, + contractId, ContractId, contractIdToTxId, Tags, transactionWitnessSetTextEnvelope, TxId, - unAddressBech32, } from "@marlowe.io/runtime-core"; import { Address } from "@marlowe.io/language-core-v1"; import { Bundle, Label, lovelace } from "@marlowe.io/marlowe-object"; @@ -108,7 +108,7 @@ async function createContractMenu(lifecycle: RuntimeLifecycle) { ); const [contractId, txId] = await createContract(lifecycle, { - payFrom: { address: unAddressBech32(walletAddress) }, + payFrom: { address: walletAddress }, payTo: { address: payee }, amount, depositDeadline, @@ -118,20 +118,43 @@ async function createContractMenu(lifecycle: RuntimeLifecycle) { console.log(`Contract created with id ${contractId}`); await waitIndicator(lifecycle.wallet, txId); - await contractMenu(); + await contractMenu(lifecycle, contractId); } -async function loadContractMenu() { - const answer = await input({ +async function loadContractMenu(lifecycle: RuntimeLifecycle) { + const cid = await input({ message: "Enter the contractId", }); - console.log(answer); - await contractMenu(); + await contractMenu(lifecycle, contractId(cid)); } -// async function contractMenu(contractId: ContractId) { -async function contractMenu() { +async function contractMenu( + lifecycle: RuntimeLifecycle, + contractId: ContractId +) { console.log("TODO: print contract state"); + const contractDetails = await lifecycle.restClient.getContractById( + contractId + ); + const now = datetoTimeout(new Date()); + + if (contractDetails.currentContract._tag === "None") { + console.log("DEBUG: current contract none"); + } + + const currentContract = + contractDetails.currentContract._tag === "None" + ? contractDetails.initialContract + : contractDetails.currentContract.value; + const nextTimeout = getNextTimeout(currentContract, now); + const oneDayFrom = (time: Timeout) => time + 24n * 60n * 60n * 1000n; // in milliseconds + const applicableInputs = await lifecycle.contracts.getApplicableInputs( + contractId, + { timeInterval: { from: now, to: nextTimeout ?? oneDayFrom(now)} } + ); + console.log("applicable inputs"); + console.log(applicableInputs); + const answer = await select({ message: "Contract menu", choices: [ @@ -159,7 +182,7 @@ async function mainLoop(lifecycle: RuntimeLifecycle) { await createContractMenu(lifecycle); break; case "load": - await loadContractMenu(); + await loadContractMenu(lifecycle); break; case "exit": process.exit(0); diff --git a/packages/language/core/v1/src/contract.ts b/packages/language/core/v1/src/contract.ts index c357b78c..294fb004 100644 --- a/packages/language/core/v1/src/contract.ts +++ b/packages/language/core/v1/src/contract.ts @@ -331,3 +331,35 @@ export function matchContract(matcher: Partial>) { } }; } +// Copied from semantic module, maybe we want to move this to a common place? An adaptor bigint maybe? +const minBigint = (a: bigint, b: bigint): bigint => (a < b ? a : b); + +/** + * This function calculates the next timeout of a contract after a given minTime. + * @param minTime Normally the current time, but it represents any time for which you want to see what is the next timeout after that. + * @param contract The contract to analyze + * @returns The next timeout after minTime, or undefined if there is no timeout after minTime. + * @category Introspection + */ +export function getNextTimeout(contract: Contract, minTime: Timeout): Timeout | undefined { + return matchContract({ + close: () => undefined, + pay: (pay) => getNextTimeout(pay.then, minTime), + if: (ifContract) => { + const thenTimeout = getNextTimeout(ifContract.then, minTime); + const elseTimeout = getNextTimeout(ifContract.else, minTime); + return thenTimeout && elseTimeout + ? minBigint(thenTimeout, elseTimeout) + : thenTimeout || elseTimeout; + }, + when: (whenContract) => { + if (minTime > whenContract.timeout) { + return getNextTimeout(whenContract.timeout_continuation, minTime); + } else { + return whenContract.timeout; + } + }, + let: (letContract) => getNextTimeout(letContract.then, minTime), + assert: (assertContract) => getNextTimeout(assertContract.then, minTime) + })(contract) +} diff --git a/packages/language/core/v1/src/index.ts b/packages/language/core/v1/src/index.ts index e85492ec..d2eec463 100644 --- a/packages/language/core/v1/src/index.ts +++ b/packages/language/core/v1/src/index.ts @@ -63,6 +63,7 @@ export { datetoTimeout, timeoutToDate, Timeout, + getNextTimeout } from "./contract.js"; export { Environment, mkEnvironment, TimeInterval } from "./environment.js"; From 7d04e34242dcf50623741b3bb2078be2f719562f Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Thu, 21 Dec 2023 11:19:27 -0300 Subject: [PATCH 05/22] wip applicable inputs --- examples/nodejs/src/applicable-inputs.ts | 253 +++++++++++++++++++++ examples/nodejs/src/marlowe-object-flow.ts | 11 +- packages/language/core/v1/src/semantics.ts | 17 +- 3 files changed, 270 insertions(+), 11 deletions(-) create mode 100644 examples/nodejs/src/applicable-inputs.ts diff --git a/examples/nodejs/src/applicable-inputs.ts b/examples/nodejs/src/applicable-inputs.ts new file mode 100644 index 00000000..f6fbe85a --- /dev/null +++ b/examples/nodejs/src/applicable-inputs.ts @@ -0,0 +1,253 @@ +import { + MarloweState, + Party, + Contract, + Deposit, + Choice, + BuiltinByteString, + Input, + ChosenNum, + Environment, + Timeout, + getNextTimeout, + datetoTimeout, + Case, + Action, + Notify, +} from "@marlowe.io/language-core-v1"; +import { + applyInput, + ContractQuiescentReduceResult, + convertReduceWarning, + Payment, + reduceContractUntilQuiescent, + TransactionWarning, +} from "@marlowe.io/language-core-v1/semantics"; +import { ContractId } from "@marlowe.io/runtime-core"; +import { RestClient } from "@marlowe.io/runtime-rest-client"; + +type ActionApplicant = Party | "anybody"; + +interface CanNotify { + type: "Notify"; + /** + * Who can make the action + */ + applicant: "anybody"; + + /** + * If the Case is merkleized, this is the continuation hash + */ + merkleizedContinuation?: BuiltinByteString; + /** + * 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; +} + +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; +} + +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; +} + +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; +} + +export type ApplicableAction = + | CanNotify + | CanDeposit + | CanChoose + | CanAdvanceTimeout; + +export async function getApplicableActions( + restClient: RestClient, + contractId: ContractId, + environment?: Environment +): Promise { + let applicableActions = [] as ApplicableAction[]; + const contractDetails = await restClient.getContractById(contractId); + + const currentContract = + contractDetails.currentContract._tag === "None" + ? contractDetails.initialContract + : contractDetails.currentContract.value; + const oneDayFrom = (time: Timeout) => time + 24n * 60n * 60n * 1000n; // in milliseconds + const now = datetoTimeout(new Date()); + const nextTimeout = getNextTimeout(currentContract, now) ?? oneDayFrom(now); + const timeInterval = { from: now, to: nextTimeout - 1n }; + + const env = environment ?? { timeInterval }; + if (contractDetails.state._tag == "None") throw new Error("State not set"); + const initialReduce = reduceContractUntilQuiescent( + env, + contractDetails.state.value, + currentContract + ); + if (initialReduce == "TEAmbiguousTimeIntervalError") + throw new Error("AmbiguousTimeIntervalError"); + 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 []; + }, + }); + } + + return applicableActions; +} + +function getApplicableInputsFromReduction( + initialReduce: ContractQuiescentReduceResult +) { + const cont = initialReduce.continuation; + if (cont == "close") return []; + if ("when" in cont) { + // cont.when + } +} + +function isDepositAction(action: Action): action is Deposit { + return "party" in action; +} + +function isNotify(action: Action): action is Notify { + return "notify_if" in action; +} + +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; + if ("merkleized_then" in cse) { + cont = await restClient.getContractSourceById({ + contractSourceId: cse.merkleized_then, + }); + } else { + cont = cse.then; + } + // 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" + + // } + } else if (isNotify(cse.case)) { + } else { + } + + // if (cse.case) +} diff --git a/examples/nodejs/src/marlowe-object-flow.ts b/examples/nodejs/src/marlowe-object-flow.ts index 0b949268..4c64df66 100644 --- a/examples/nodejs/src/marlowe-object-flow.ts +++ b/examples/nodejs/src/marlowe-object-flow.ts @@ -22,6 +22,7 @@ import { Address } from "@marlowe.io/language-core-v1"; 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"; main(); // #region Interactive menu @@ -118,7 +119,7 @@ async function createContractMenu(lifecycle: RuntimeLifecycle) { console.log(`Contract created with id ${contractId}`); await waitIndicator(lifecycle.wallet, txId); - await contractMenu(lifecycle, contractId); + // await contractMenu(lifecycle, contractId); } async function loadContractMenu(lifecycle: RuntimeLifecycle) { @@ -146,14 +147,16 @@ async function contractMenu( contractDetails.currentContract._tag === "None" ? contractDetails.initialContract : contractDetails.currentContract.value; - const nextTimeout = getNextTimeout(currentContract, now); const oneDayFrom = (time: Timeout) => time + 24n * 60n * 60n * 1000n; // in milliseconds + const nextTimeout = getNextTimeout(currentContract, now) ?? oneDayFrom(now); + const timeInterval = { from: now, to: nextTimeout - 1n }; + console.log("time interval", timeInterval); const applicableInputs = await lifecycle.contracts.getApplicableInputs( contractId, - { timeInterval: { from: now, to: nextTimeout ?? oneDayFrom(now)} } + { timeInterval } ); console.log("applicable inputs"); - console.log(applicableInputs); + console.log(MarloweJSON.stringify(applicableInputs, null, 2)); const answer = await select({ message: "Contract menu", diff --git a/packages/language/core/v1/src/semantics.ts b/packages/language/core/v1/src/semantics.ts index 45f56c03..cf96dc96 100644 --- a/packages/language/core/v1/src/semantics.ts +++ b/packages/language/core/v1/src/semantics.ts @@ -533,21 +533,24 @@ function reduceContractStep( /** * @hidden */ -type ReduceResult = - | { +export type ContractQuiescentReduceResult = + { type: "ContractQuiescent"; reduced: boolean; state: MarloweState; warnings: ReduceWarning[]; payments: Payment[]; continuation: Contract; - } - | AmbiguousTimeIntervalError; + } +/** + * @hidden + */ +type ReduceResult = ContractQuiescentReduceResult| AmbiguousTimeIntervalError; /** * @hidden */ -function reduceContractUntilQuiescent( +export function reduceContractUntilQuiescent( env: Environment, state: MarloweState, cont: Contract @@ -749,7 +752,7 @@ function applyCases( /** * @hidden */ -function applyInput( +export function applyInput( env: Environment, state: MarloweState, input: Input, @@ -770,7 +773,7 @@ type TransactionWarning = | Shadowing | AssertionFailed; -function convertReduceWarning(warnings: ReduceWarning[]): TransactionWarning[] { +export function convertReduceWarning(warnings: ReduceWarning[]): TransactionWarning[] { return warnings.filter((w) => w !== "NoWarning") as TransactionWarning[]; } From 14e3cc451698f6a9574d0a17076ffc483d03d2a6 Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Thu, 21 Dec 2023 23:05:14 -0300 Subject: [PATCH 06/22] 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 cf96dc96..b334c989 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, From 7bf8ff37f041c4306815451bb9612b48c3c500db Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Thu, 4 Jan 2024 11:45:24 -0300 Subject: [PATCH 07/22] Add applicable action filter --- examples/nodejs/.gitignore | 2 +- examples/nodejs/src/applicable-inputs.ts | 70 ++++++++++++++--- examples/nodejs/src/config.ts | 4 +- examples/nodejs/src/marlowe-object-flow.ts | 88 +++++++++++++++++----- 4 files changed, 133 insertions(+), 31 deletions(-) diff --git a/examples/nodejs/.gitignore b/examples/nodejs/.gitignore index a9b8cc8b..3c1c56bb 100644 --- a/examples/nodejs/.gitignore +++ b/examples/nodejs/.gitignore @@ -1 +1 @@ -.config.json +*.config.json diff --git a/examples/nodejs/src/applicable-inputs.ts b/examples/nodejs/src/applicable-inputs.ts index 153e9b40..5d2a480f 100644 --- a/examples/nodejs/src/applicable-inputs.ts +++ b/examples/nodejs/src/applicable-inputs.ts @@ -1,3 +1,9 @@ +/** +This is an experimental module that aims to replace the current `next` features. +TODO: After PR-8891 and as part of PLT-9054 move these to the runtime-lifecycle package + and create a github discussion to modify the backend. + */ + import { MarloweState, Party, @@ -17,6 +23,7 @@ import { IChoice, INotify, InputContent, + RoleName, } from "@marlowe.io/language-core-v1"; import { applyAllInputs, @@ -28,8 +35,9 @@ import { reduceContractUntilQuiescent, TransactionWarning, } from "@marlowe.io/language-core-v1/semantics"; -import { ContractId } from "@marlowe.io/runtime-core"; +import { AddressBech32, ContractId, PolicyId } from "@marlowe.io/runtime-core"; import { RestClient } from "@marlowe.io/runtime-rest-client"; +import { WalletAPI } from "@marlowe.io/wallet"; type ActionApplicant = Party | "anybody"; @@ -63,13 +71,13 @@ interface AppliedActionResult { interface CanNotify { type: "Notify"; - + policyId: PolicyId; applyAction(): AppliedActionResult; } interface CanDeposit { type: "Deposit"; - + policyId: PolicyId; deposit: Deposit; applyAction(): AppliedActionResult; @@ -77,7 +85,7 @@ interface CanDeposit { interface CanChoose { type: "Choice"; - + policyId: PolicyId; choice: Choice; applyAction(choice: ChosenNum): AppliedActionResult; @@ -85,7 +93,7 @@ interface CanChoose { interface CanAdvanceTimeout { type: "AdvanceTimeout"; - + policyId: PolicyId; applyAction(): AppliedActionResult; } @@ -140,6 +148,7 @@ export async function getApplicableActions( if (initialReduce.reduced) { applicableActions.push({ type: "AdvanceTimeout", + policyId: contractDetails.roleTokenMintingPolicyId, applyAction() { return { inputs: [], @@ -164,7 +173,8 @@ export async function getApplicableActions( initialReduce.state, initialReduce.payments, convertReduceWarning(initialReduce.warnings), - cse + cse, + contractDetails.roleTokenMintingPolicyId ) ) ); @@ -176,6 +186,45 @@ export async function getApplicableActions( return applicableActions; } +export async function mkPartyFilter(wallet: WalletAPI) { + const address = await wallet.getUsedAddresses(); + const tokens = await wallet.getTokens(); + let tokenMap = new Map(); + function getTokenMap() { + if (tokenMap.size === 0 && tokens.length > 0) { + tokens.forEach(token => { + // Role tokens only have 1 element + if (token.quantity > 1) return; + + const currentTokens = tokenMap.get(token.assetId.policyId) ?? []; + currentTokens.push(token.assetId.assetName); + tokenMap.set(token.assetId.policyId, currentTokens); + }) + } + return tokenMap; + } + + return (party: Party, policyId: PolicyId) => { + if ("role_token" in party) { + const policyTokens = getTokenMap().get(policyId); + if (policyTokens === undefined) return false; + return policyTokens.includes(party.role_token); + } else { + return address.includes(party.address as AddressBech32) + } + } +} + +export async function mkApplicableActionsFilter( + wallet: WalletAPI +) { + const partyFilter = await mkPartyFilter(wallet); + return (action: ApplicableAction) => { + const applicant = getApplicant(action); + if (applicant === "anybody") return true; + return partyFilter(applicant, action.policyId); + } +} function isDepositAction(action: Action): action is Deposit { return "party" in action; @@ -196,7 +245,8 @@ async function getApplicableActionFromCase( state: MarloweState, previousPayments: Payment[], previousWarnings: TransactionWarning[], - cse: Case + cse: Case, + policyId: PolicyId ): Promise { let cseContinuation: Contract; if ("merkleized_then" in cse) { @@ -232,7 +282,7 @@ async function getApplicableActionFromCase( return { type: "Deposit", deposit, - + policyId, applyAction() { const input = decorateInput({ input_from_party: deposit.party, @@ -261,7 +311,7 @@ async function getApplicableActionFromCase( return { type: "Choice", choice, - + policyId, applyAction(chosenNum: ChosenNum) { if (!inBounds(chosenNum, choice.choose_between)) { throw new Error("Chosen number is not in bounds"); @@ -292,7 +342,7 @@ async function getApplicableActionFromCase( return { type: "Notify", - + policyId, applyAction() { const input = decorateInput("input_notify"); // TODO: Re-check if this env should be the same as the initial env or a new one. diff --git a/examples/nodejs/src/config.ts b/examples/nodejs/src/config.ts index 89a65c5c..03d31550 100644 --- a/examples/nodejs/src/config.ts +++ b/examples/nodejs/src/config.ts @@ -19,8 +19,8 @@ const configGuard = t.type({ export type Config = t.TypeOf; -export async function readConfig(): Promise { - const configStr = await fs.readFile("./.config.json", { encoding: "utf-8" }); +export async function readConfig(path = "./.config.json"): Promise { + const configStr = await fs.readFile(path, { encoding: "utf-8" }); const result = configGuard.decode(JSON.parse(configStr)); if (result._tag === "Left") { throw new Error("Invalid config.json"); diff --git a/examples/nodejs/src/marlowe-object-flow.ts b/examples/nodejs/src/marlowe-object-flow.ts index fe6a4a7c..dc153774 100644 --- a/examples/nodejs/src/marlowe-object-flow.ts +++ b/examples/nodejs/src/marlowe-object-flow.ts @@ -8,7 +8,11 @@ import { mkLucidWallet, WalletAPI } from "@marlowe.io/wallet"; import { mkRuntimeLifecycle } from "@marlowe.io/runtime-lifecycle"; import { Lucid, Blockfrost, C } from "lucid-cardano"; import { readConfig } from "./config.js"; -import { datetoTimeout, getNextTimeout, Timeout } from "@marlowe.io/language-core-v1"; +import { + datetoTimeout, + getNextTimeout, + Timeout, +} from "@marlowe.io/language-core-v1"; import { addressBech32, contractId, @@ -24,10 +28,42 @@ 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"; +import { + ApplicableAction, + getApplicableActions, + mkApplicableActionsFilter, + mkPartyFilter, +} from "./applicable-inputs.js"; +import arg from "arg"; + +const args = arg({ + "--help": Boolean, + "--config": String, + "-c": "--config", +}); + +if (args["--help"]) { + printHelp(0); +} main(); // #region Interactive menu +function printHelp(exitStatus: number): never { + console.log( + "Usage: npm run marlowe-object-flow -- --config " + ); + console.log(""); + console.log("Example:"); + console.log( + " npm run marlowe-object-flow -- --config alice.config" + ); + console.log("Options:"); + console.log(" --help: Print this message"); + console.log(" --config | -c: The path to the config file [default .config.json]"); + process.exit(exitStatus); +} + + async function waitIndicator(wallet: WalletAPI, txId: TxId) { process.stdout.write("Waiting for the transaction to be confirmed..."); let done = false; @@ -131,7 +167,11 @@ async function loadContractMenu(lifecycle: RuntimeLifecycle) { await contractMenu(lifecycle, contractId(cid)); } -async function debugGetNext(lifecycle: RuntimeLifecycle, contractDetails: ContractDetails, contractId: ContractId) { +async function debugGetNext( + lifecycle: RuntimeLifecycle, + contractDetails: ContractDetails, + contractId: ContractId +) { const now = datetoTimeout(new Date()); if (contractDetails.currentContract._tag === "None") { @@ -152,9 +192,25 @@ async function debugGetNext(lifecycle: RuntimeLifecycle, contractDetails: Contra ); console.log("applicable inputs"); console.log(MarloweJSON.stringify(applicableInputs, null, 2)); - } +function debugApplicableActions(applicableActions: ApplicableAction[]) { + 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)); + }); +} async function contractMenu( lifecycle: RuntimeLifecycle, contractId: ContractId @@ -163,21 +219,17 @@ async function contractMenu( 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 applicableActions = await getApplicableActions( + lifecycle.restClient, + contractId + ); + const myActionsFilter = await mkApplicableActionsFilter(lifecycle.wallet); + const choices = applicableActions.filter(myActionsFilter) + debugApplicableActions(choices); + const answer = await select({ message: "Contract menu", @@ -324,7 +376,7 @@ function mkDelayPayment(schema: DelayPaymentSchema): ContractBundle { } async function main() { - const config = await readConfig(); + const config = await readConfig(args["--config"]); const lucid = await Lucid.new( new Blockfrost(config.blockfrostUrl, config.blockfrostProjectId), config.network From 9c8b732c3edd9bd84d20baa42141e010c5f94544 Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Thu, 4 Jan 2024 16:23:09 -0300 Subject: [PATCH 08/22] Flatten Notify, choices and deposit in applicable actions --- examples/nodejs/src/applicable-inputs.ts | 226 +++++++++++++++++---- examples/nodejs/src/marlowe-object-flow.ts | 2 +- 2 files changed, 191 insertions(+), 37 deletions(-) diff --git a/examples/nodejs/src/applicable-inputs.ts b/examples/nodejs/src/applicable-inputs.ts index 5d2a480f..67be2d8d 100644 --- a/examples/nodejs/src/applicable-inputs.ts +++ b/examples/nodejs/src/applicable-inputs.ts @@ -24,6 +24,8 @@ import { INotify, InputContent, RoleName, + Token, + Bound, } from "@marlowe.io/language-core-v1"; import { applyAllInputs, @@ -38,7 +40,8 @@ import { import { AddressBech32, ContractId, PolicyId } from "@marlowe.io/runtime-core"; import { RestClient } from "@marlowe.io/runtime-rest-client"; import { WalletAPI } from "@marlowe.io/wallet"; - +import { Monoid } from "fp-ts/lib/Monoid.js"; +import * as R from "fp-ts/lib/Record.js"; type ActionApplicant = Party | "anybody"; interface AppliedActionResult { @@ -91,22 +94,22 @@ interface CanChoose { applyAction(choice: ChosenNum): AppliedActionResult; } -interface CanAdvanceTimeout { - type: "AdvanceTimeout"; +/** + * This Applicable action is intended to be used when the contract is not in a quiescent state. + * This means that the contract is either timeouted, or it was just created and it doesn't starts with a `When` + */ +interface CanAdvance { + type: "Advance"; policyId: PolicyId; applyAction(): AppliedActionResult; } -export type ApplicableAction = - | CanNotify - | CanDeposit - | CanChoose - | CanAdvanceTimeout; +export type ApplicableAction = CanNotify | CanDeposit | CanChoose | CanAdvance; function getApplicant(action: ApplicableAction): ActionApplicant { switch (action.type) { case "Notify": - case "AdvanceTimeout": + case "Advance": return "anybody"; case "Deposit": return action.deposit.party; @@ -147,7 +150,7 @@ export async function getApplicableActions( throw new Error("AmbiguousTimeIntervalError"); if (initialReduce.reduced) { applicableActions.push({ - type: "AdvanceTimeout", + type: "Advance", policyId: contractDetails.roleTokenMintingPolicyId, applyAction() { return { @@ -178,11 +181,16 @@ export async function getApplicableActions( ) ) ); - applicableActions = applicableActions.concat(applicableActionsFromCases.filter(x => x !== undefined) as ApplicableAction[]); - + applicableActions = applicableActions.concat( + toApplicableActions( + applicableActionsFromCases.reduce( + mergeApplicableActionAccumulator.concat, + mergeApplicableActionAccumulator.empty + ) + ) + ); } - return applicableActions; } @@ -192,14 +200,14 @@ export async function mkPartyFilter(wallet: WalletAPI) { let tokenMap = new Map(); function getTokenMap() { if (tokenMap.size === 0 && tokens.length > 0) { - tokens.forEach(token => { + tokens.forEach((token) => { // Role tokens only have 1 element if (token.quantity > 1) return; const currentTokens = tokenMap.get(token.assetId.policyId) ?? []; currentTokens.push(token.assetId.assetName); tokenMap.set(token.assetId.policyId, currentTokens); - }) + }); } return tokenMap; } @@ -210,20 +218,18 @@ export async function mkPartyFilter(wallet: WalletAPI) { if (policyTokens === undefined) return false; return policyTokens.includes(party.role_token); } else { - return address.includes(party.address as AddressBech32) + return address.includes(party.address as AddressBech32); } - } + }; } -export async function mkApplicableActionsFilter( - wallet: WalletAPI -) { +export async function mkApplicableActionsFilter(wallet: WalletAPI) { const partyFilter = await mkPartyFilter(wallet); return (action: ApplicableAction) => { const applicant = getApplicant(action); if (applicant === "anybody") return true; return partyFilter(applicant, action.policyId); - } + }; } function isDepositAction(action: Action): action is Deposit { @@ -238,6 +244,148 @@ function isChoice(action: Action): action is Choice { return "choose_between" in action; } +/** + * Internal data structure to be able to accumulate and later flatter applicable actions + * @hidden + */ +type ApplicableActionAccumulator = { + deposits: Record; + choices: Record; + notifies: CanNotify | undefined; +}; + +const toApplicableActions = ( + accumulator: ApplicableActionAccumulator +): ApplicableAction[] => { + const deposits = Object.values(accumulator.deposits); + const choices = Object.values(accumulator.choices); + const notifies = accumulator.notifies ? [accumulator.notifies] : []; + return [...deposits, ...choices, ...notifies]; +}; + +const flattenDeposits = { + concat: (fst: CanDeposit, snd: CanDeposit) => { + return fst; + }, +}; + +const partyKey = (party: Party) => { + if ("role_token" in party) { + return `role_${party.role_token}`; + } else { + return `address_${party.address}`; + } +}; + +const tokenKey = (token: Token) => { + return `${token.currency_symbol}-${token.token_name}`; +}; +const accumulatorFromDeposit = ( + env: Environment, + state: MarloweState, + action: CanDeposit +) => { + const { party, into_account, of_token, deposits } = action.deposit; + const value = evalValue(env, state, deposits); + + const depositKey = `${partyKey(party)}-${partyKey(into_account)}-${tokenKey( + of_token + )}-${value}`; + return { + deposits: { [depositKey]: action }, + choices: {}, + notifies: undefined, + }; +}; + +const accumulatorFromChoice = (action: CanChoose) => { + const { for_choice } = action.choice; + const choiceKey = `${partyKey(for_choice.choice_owner)}-${ + for_choice.choice_name + }`; + return { + deposits: {}, + choices: { [choiceKey]: action }, + notifies: undefined, + }; +}; + +const accumulatorFromNotify = (action: CanNotify) => { + return { + deposits: {}, + choices: {}, + notifies: action, + }; +}; +// TODO: Move to adapter +const minBigint = (a: bigint, b: bigint): bigint => (a < b ? a : b); +const maxBigint = (a: bigint, b: bigint): bigint => (a > b ? a : b); + +function mergeBounds(bounds: Bound[]): Bound[] { + const mergedBounds: Bound[] = []; + + const sortedBounds = [...bounds].sort((a, b) => (a.from > b.from ? 1 : a.from < b.from ? -1 : 0)); + + let currentBound: Bound | null = null; + + for (const bound of sortedBounds) { + if (currentBound === null) { + currentBound = {...bound}; + } else { + if (bound.from <= currentBound.to) { + currentBound.to = maxBigint(currentBound.to, bound.to); + } else { + mergedBounds.push(currentBound); + currentBound = {...bound}; + } + } + } + + if (currentBound !== null) { + mergedBounds.push(currentBound); + } + + return mergedBounds; +} + +const flattenChoices = { + concat: (fst: CanChoose, snd: CanChoose): CanChoose => { + const mergedBounds = mergeBounds( + fst.choice.choose_between.concat(snd.choice.choose_between) + ); + return { + type: "Choice", + policyId: fst.policyId, + choice: { + for_choice: fst.choice.for_choice, + choose_between: mergedBounds, + }, + applyAction: (chosenNum: ChosenNum) => { + if (inBounds(chosenNum, fst.choice.choose_between)) { + return fst.applyAction(chosenNum); + } else { + return snd.applyAction(chosenNum); + } + }, + }; + }, +}; + +const mergeApplicableActionAccumulator: Monoid = { + empty: { + deposits: {}, + choices: {}, + notifies: undefined, + }, + concat: (fst, snd) => { + return { + deposits: R.union(flattenDeposits)(fst.deposits)(snd.deposits), + choices: R.union(flattenChoices)(fst.choices)(snd.choices), + notifies: fst.notifies ?? snd.notifies, + }; + }, +}; + async function getApplicableActionFromCase( restClient: RestClient, env: Environment, @@ -247,7 +395,7 @@ async function getApplicableActionFromCase( previousWarnings: TransactionWarning[], cse: Case, policyId: PolicyId -): Promise { +): Promise { let cseContinuation: Contract; if ("merkleized_then" in cse) { cseContinuation = await restClient.getContractSourceById({ @@ -260,8 +408,8 @@ async function getApplicableActionFromCase( if ("merkleized_then" in cse) { const merkleizedHashAndContinuation = { continuation_hash: cse.merkleized_then, - merkleized_continuation: cseContinuation - } + merkleized_continuation: cseContinuation, + }; // MerkleizedNotify are serialized as the plain merkle object if (content === "input_notify") { return merkleizedHashAndContinuation; @@ -269,8 +417,8 @@ async function getApplicableActionFromCase( // For IDeposit and IChoice is the InputContent + the merkle object return { ...merkleizedHashAndContinuation, - ...content - } + ...content, + }; } } else { return content; @@ -279,7 +427,7 @@ async function getApplicableActionFromCase( if (isDepositAction(cse.case)) { const deposit = cse.case; - return { + return accumulatorFromDeposit(env, state, { type: "Deposit", deposit, policyId, @@ -291,7 +439,9 @@ async function getApplicableActionFromCase( 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]); + const appliedInput = applyAllInputs(env, state, currentContract, [ + input, + ]); // TODO: Improve error handling if (typeof appliedInput === "string") throw new Error(appliedInput); @@ -304,11 +454,11 @@ async function getApplicableActionFromCase( payments: [...previousPayments, ...appliedInput.payments], }; }, - }; + }); } else if (isChoice(cse.case)) { const choice = cse.case; - return { + return accumulatorFromChoice({ type: "Choice", choice, policyId, @@ -321,7 +471,9 @@ async function getApplicableActionFromCase( 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]); + const appliedInput = applyAllInputs(env, state, currentContract, [ + input, + ]); // TODO: Improve error handling if (typeof appliedInput === "string") throw new Error(appliedInput); return { @@ -333,20 +485,22 @@ async function getApplicableActionFromCase( payments: [...previousPayments, ...appliedInput.payments], }; }, - }; + }); } else { const notify = cse.case; if (!evalObservation(env, state, notify.notify_if)) { - return; + return mergeApplicableActionAccumulator.empty; } - return { + return accumulatorFromNotify({ type: "Notify", policyId, 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]); + const appliedInput = applyAllInputs(env, state, currentContract, [ + input, + ]); // TODO: Improve error handling if (typeof appliedInput === "string") throw new Error(appliedInput); return { @@ -358,6 +512,6 @@ async function getApplicableActionFromCase( payments: [...previousPayments, ...appliedInput.payments], }; }, - }; + }); } } diff --git a/examples/nodejs/src/marlowe-object-flow.ts b/examples/nodejs/src/marlowe-object-flow.ts index dc153774..5ac3ebcd 100644 --- a/examples/nodejs/src/marlowe-object-flow.ts +++ b/examples/nodejs/src/marlowe-object-flow.ts @@ -220,7 +220,6 @@ async function contractMenu( contractId ); - // await debugGetNext(lifecycle, contractDetails, contractId); const applicableActions = await getApplicableActions( lifecycle.restClient, @@ -228,6 +227,7 @@ async function contractMenu( ); const myActionsFilter = await mkApplicableActionsFilter(lifecycle.wallet); const choices = applicableActions.filter(myActionsFilter) + await debugGetNext(lifecycle, contractDetails, contractId); debugApplicableActions(choices); From a25de022837c96b38bfa05e45bcec8a480dd9f4b Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Fri, 5 Jan 2024 10:30:12 -0300 Subject: [PATCH 09/22] Add contractMenu flow --- examples/nodejs/src/applicable-inputs.ts | 2 +- examples/nodejs/src/marlowe-object-flow.ts | 61 +++++++++++++++++----- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/examples/nodejs/src/applicable-inputs.ts b/examples/nodejs/src/applicable-inputs.ts index 67be2d8d..e2a81505 100644 --- a/examples/nodejs/src/applicable-inputs.ts +++ b/examples/nodejs/src/applicable-inputs.ts @@ -44,7 +44,7 @@ import { Monoid } from "fp-ts/lib/Monoid.js"; import * as R from "fp-ts/lib/Record.js"; type ActionApplicant = Party | "anybody"; -interface AppliedActionResult { +export interface AppliedActionResult { /** * What inputs needs to be provided to apply the action */ diff --git a/examples/nodejs/src/marlowe-object-flow.ts b/examples/nodejs/src/marlowe-object-flow.ts index 5ac3ebcd..9a8accb6 100644 --- a/examples/nodejs/src/marlowe-object-flow.ts +++ b/examples/nodejs/src/marlowe-object-flow.ts @@ -30,11 +30,13 @@ import { MarloweJSON } from "@marlowe.io/adapter/codec"; import { ContractDetails } from "@marlowe.io/runtime-rest-client/contract"; import { ApplicableAction, + AppliedActionResult, getApplicableActions, mkApplicableActionsFilter, mkPartyFilter, } from "./applicable-inputs.js"; import arg from "arg"; +import { posixTimeToIso8601 } from "@marlowe.io/adapter/time"; const args = arg({ "--help": Boolean, @@ -167,6 +169,7 @@ async function loadContractMenu(lifecycle: RuntimeLifecycle) { await contractMenu(lifecycle, contractId(cid)); } +// TODO: Delete async function debugGetNext( lifecycle: RuntimeLifecycle, contractDetails: ContractDetails, @@ -194,6 +197,7 @@ async function debugGetNext( console.log(MarloweJSON.stringify(applicableInputs, null, 2)); } +// TODO: Delete function debugApplicableActions(applicableActions: ApplicableAction[]) { applicableActions.forEach((action) => { console.log("***"); @@ -214,32 +218,63 @@ function debugApplicableActions(applicableActions: ApplicableAction[]) { async function contractMenu( lifecycle: RuntimeLifecycle, contractId: ContractId -) { +): Promise { console.log("TODO: print contract state"); const contractDetails = await lifecycle.restClient.getContractById( contractId ); - const applicableActions = await getApplicableActions( lifecycle.restClient, contractId ); const myActionsFilter = await mkApplicableActionsFilter(lifecycle.wallet); - const choices = applicableActions.filter(myActionsFilter) - await debugGetNext(lifecycle, contractDetails, contractId); - debugApplicableActions(choices); - + const myActions = applicableActions.filter(myActionsFilter) + // await debugGetNext(lifecycle, contractDetails, contractId); + // debugApplicableActions(myActions); + + const choices: Array<{name: string, value: {actionType: string, results?: AppliedActionResult}}> = [ + { name: "Re-check contract state", value: {actionType: "check-state", results: undefined} }, + ...myActions.map(action => { + switch (action.type) { + case "Advance": + // TODO: Disambiguate between timeout deposit and release + return { name: "Advance contract", value: {actionType: "advance", results: action.applyAction() }} + case "Deposit": + return { name: `Deposit ${action.deposit.deposits} lovelaces`, value: {actionType: "deposit", results: action.applyAction()} } + default: + throw new Error("Unexpected action type") + } + }), + { name: "Return to main menu", value: {actionType: "return", results: undefined} }, + ] - const answer = await select({ + const action = await select({ message: "Contract menu", - choices: [ - { name: "Re-check contract state", value: "check-state" }, - { name: "Deposit", value: "deposit" }, - { name: "Release funds", value: "release" }, - { name: "Return to main menu", value: "return" }, - ], + choices, }); + let txId + switch (action.actionType) { + case "check-state": + return contractMenu(lifecycle, contractId) + case "advance": + if (!action.results) throw new Error("This should not happen") + console.log("Advancing contract"); + txId = await lifecycle.contracts.applyInputs(contractId, {inputs: action.results.inputs, invalidBefore: posixTimeToIso8601(action.results.environment.timeInterval.from), invalidHereafter: posixTimeToIso8601(action.results.environment.timeInterval.to)}) + console.log(`Input applied with txId ${txId}`) + await waitIndicator(lifecycle.wallet, txId); + return contractMenu(lifecycle, contractId) + case "deposit": + // TODO: Remove duplication + if (!action.results) throw new Error("This should not happen") + console.log("Making deposit"); + txId = await lifecycle.contracts.applyInputs(contractId, {inputs: action.results.inputs, invalidBefore: posixTimeToIso8601(action.results.environment.timeInterval.from), invalidHereafter: posixTimeToIso8601(action.results.environment.timeInterval.to)}) + console.log(`Input applied with txId ${txId}`) + await waitIndicator(lifecycle.wallet, txId); + return contractMenu(lifecycle, contractId) + case "return": + return; + } } async function mainLoop(lifecycle: RuntimeLifecycle) { From 810a7d4ffcc7155849c0d2436abff1734367dea7 Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Fri, 5 Jan 2024 12:48:39 -0300 Subject: [PATCH 10/22] Add state to the object flow --- .../applicable-inputs.ts | 10 +- .../src/experimental-features/metadata.ts | 8 + examples/nodejs/src/marlowe-object-flow.ts | 350 ++++++++++++------ packages/adapter/src/time.ts | 4 + packages/language/core/v1/src/semantics.ts | 2 + packages/language/core/v1/src/transaction.ts | 17 +- .../runtime/client/rest/src/pagination.ts | 5 +- packages/runtime/lifecycle/src/api.ts | 7 + .../lifecycle/src/generic/contracts.ts | 53 +++ 9 files changed, 331 insertions(+), 125 deletions(-) rename examples/nodejs/src/{ => experimental-features}/applicable-inputs.ts (98%) create mode 100644 examples/nodejs/src/experimental-features/metadata.ts diff --git a/examples/nodejs/src/applicable-inputs.ts b/examples/nodejs/src/experimental-features/applicable-inputs.ts similarity index 98% rename from examples/nodejs/src/applicable-inputs.ts rename to examples/nodejs/src/experimental-features/applicable-inputs.ts index e2a81505..92def106 100644 --- a/examples/nodejs/src/applicable-inputs.ts +++ b/examples/nodejs/src/experimental-features/applicable-inputs.ts @@ -127,23 +127,23 @@ export async function getApplicableActions( const contractDetails = await restClient.getContractById(contractId); const currentContract = - contractDetails.currentContract._tag === "None" - ? contractDetails.initialContract - : contractDetails.currentContract.value; + contractDetails.currentContract + ? contractDetails.currentContract + : contractDetails.initialContract; const oneDayFrom = (time: Timeout) => time + 24n * 60n * 60n * 1000n; // in milliseconds const now = datetoTimeout(new Date()); const nextTimeout = getNextTimeout(currentContract, now) ?? oneDayFrom(now); const timeInterval = { from: now, to: nextTimeout - 1n }; const env = environment ?? { timeInterval }; - if (contractDetails.state._tag == "None") { + if (typeof contractDetails.state === "undefined") { // 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, + contractDetails.state, currentContract ); if (initialReduce == "TEAmbiguousTimeIntervalError") diff --git a/examples/nodejs/src/experimental-features/metadata.ts b/examples/nodejs/src/experimental-features/metadata.ts new file mode 100644 index 00000000..e7c8e711 --- /dev/null +++ b/examples/nodejs/src/experimental-features/metadata.ts @@ -0,0 +1,8 @@ +import { Address } from "@marlowe.io/language-core-v1"; + +export const splitAddress = ({ address }: Address) => { + const halfLength = Math.floor(address.length / 2); + const s1 = address.substring(0, halfLength); + const s2 = address.substring(halfLength); + return [s1, s2]; +}; diff --git a/examples/nodejs/src/marlowe-object-flow.ts b/examples/nodejs/src/marlowe-object-flow.ts index 9a8accb6..50a8d411 100644 --- a/examples/nodejs/src/marlowe-object-flow.ts +++ b/examples/nodejs/src/marlowe-object-flow.ts @@ -14,7 +14,6 @@ import { Timeout, } from "@marlowe.io/language-core-v1"; import { - addressBech32, contractId, ContractId, contractIdToTxId, @@ -33,11 +32,13 @@ import { AppliedActionResult, getApplicableActions, mkApplicableActionsFilter, - mkPartyFilter, -} from "./applicable-inputs.js"; +} from "./experimental-features/applicable-inputs.js"; import arg from "arg"; -import { posixTimeToIso8601 } from "@marlowe.io/adapter/time"; - +import { POSIXTime, posixTimeToIso8601 } from "@marlowe.io/adapter/time"; +import { splitAddress } from "./experimental-features/metadata.js"; +import { SingleInputTx } from "../../../packages/language/core/v1/dist/esm/transaction.js"; +import * as t from "io-ts/lib/index.js" +import { deepEqual } from "@marlowe.io/adapter/deep-equal"; const args = arg({ "--help": Boolean, "--config": String, @@ -148,98 +149,85 @@ async function createContractMenu(lifecycle: RuntimeLifecycle) { `The payment must be deposited by ${depositDeadline} and will be released to ${payee} by ${releaseDeadline}` ); - const [contractId, txId] = await createContract(lifecycle, { + const scheme = { payFrom: { address: walletAddress }, payTo: { address: payee }, amount, depositDeadline, releaseDeadline, - }); + }; + const [contractId, txId] = await createContract(lifecycle, scheme); console.log(`Contract created with id ${contractId}`); + await waitIndicator(lifecycle.wallet, txId); - // await contractMenu(lifecycle, contractId); + await contractMenu(lifecycle, scheme, contractId); } async function loadContractMenu(lifecycle: RuntimeLifecycle) { - const cid = await input({ + const cidStr = await input({ message: "Enter the contractId", }); - await contractMenu(lifecycle, contractId(cid)); -} + const cid = contractId(cidStr); -// TODO: Delete -async function debugGetNext( - lifecycle: RuntimeLifecycle, - contractDetails: ContractDetails, - contractId: ContractId -) { - const now = datetoTimeout(new Date()); + const contractDetails = await lifecycle.restClient.getContractById( + cid + ); - if (contractDetails.currentContract._tag === "None") { - console.log("DEBUG: current contract none"); - } + const scheme = extractSchemeFromTags(contractDetails.tags); + console.log("Contract details:"); + console.log(` * Pay from: ${scheme.payFrom.address}`); + console.log(` * Pay to: ${scheme.payTo.address}`); + console.log(` * Amount: ${scheme.amount} lovelaces`); + console.log(` * Deposit deadline: ${scheme.depositDeadline}`); + console.log(` * Release deadline: ${scheme.releaseDeadline}`); - const currentContract = - contractDetails.currentContract._tag === "None" - ? contractDetails.initialContract - : contractDetails.currentContract.value; - const oneDayFrom = (time: Timeout) => time + 24n * 60n * 60n * 1000n; // in milliseconds - const nextTimeout = getNextTimeout(currentContract, now) ?? oneDayFrom(now); - const timeInterval = { from: now, to: nextTimeout - 1n }; - console.log("time interval", timeInterval); - const applicableInputs = await lifecycle.contracts.getApplicableInputs( - contractId, - { timeInterval } + const contractBundle = mkDelayPayment(scheme); + const {contractSourceId} = await lifecycle.restClient.createContractSources( + contractBundle.main, + contractBundle.bundle ); - console.log("applicable inputs"); - console.log(MarloweJSON.stringify(applicableInputs, null, 2)); -} + const initialContract = await lifecycle.restClient.getContractSourceById({ contractSourceId} ); -// TODO: Delete -function debugApplicableActions(applicableActions: ApplicableAction[]) { - 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)); - }); + if (!deepEqual(initialContract, contractDetails.initialContract)) { + throw new Error("The contract on chain does not match the expected contract for the scheme"); + }; + + return contractMenu(lifecycle, scheme, cid); } + async function contractMenu( lifecycle: RuntimeLifecycle, + scheme: DelayPaymentScheme, contractId: ContractId ): Promise { - console.log("TODO: print contract state"); - const contractDetails = await lifecycle.restClient.getContractById( - contractId - ); + const inputHistory = await lifecycle.contracts.getInputHistory(contractId); + + const contractState = getState(scheme, new Date(), inputHistory); + printState(contractState, scheme); + if (contractState.type === "Closed") return; const applicableActions = await getApplicableActions( lifecycle.restClient, contractId ); + const myActionsFilter = await mkApplicableActionsFilter(lifecycle.wallet); const myActions = applicableActions.filter(myActionsFilter) - // await debugGetNext(lifecycle, contractDetails, contractId); - // debugApplicableActions(myActions); const choices: Array<{name: string, value: {actionType: string, results?: AppliedActionResult}}> = [ { name: "Re-check contract state", value: {actionType: "check-state", results: undefined} }, ...myActions.map(action => { switch (action.type) { case "Advance": - // TODO: Disambiguate between timeout deposit and release - return { name: "Advance contract", value: {actionType: "advance", results: action.applyAction() }} + + return { + name: "Close contract", + description: contractState.type == "PaymentMissed" ? "The payer will receive minUTXO" : "The payer will receive minUTXO and the payee will receive the payment", + value: {actionType: "advance", results: action.applyAction() } + } + case "Deposit": return { name: `Deposit ${action.deposit.deposits} lovelaces`, value: {actionType: "deposit", results: action.applyAction()} } default: @@ -256,22 +244,22 @@ async function contractMenu( let txId switch (action.actionType) { case "check-state": - return contractMenu(lifecycle, contractId) + return contractMenu(lifecycle, scheme, contractId) case "advance": if (!action.results) throw new Error("This should not happen") console.log("Advancing contract"); - txId = await lifecycle.contracts.applyInputs(contractId, {inputs: action.results.inputs, invalidBefore: posixTimeToIso8601(action.results.environment.timeInterval.from), invalidHereafter: posixTimeToIso8601(action.results.environment.timeInterval.to)}) + txId = await lifecycle.contracts.applyInputs(contractId, {inputs: action.results.inputs}) console.log(`Input applied with txId ${txId}`) await waitIndicator(lifecycle.wallet, txId); - return contractMenu(lifecycle, contractId) + return contractMenu(lifecycle, scheme, contractId) case "deposit": // TODO: Remove duplication if (!action.results) throw new Error("This should not happen") console.log("Making deposit"); - txId = await lifecycle.contracts.applyInputs(contractId, {inputs: action.results.inputs, invalidBefore: posixTimeToIso8601(action.results.environment.timeInterval.from), invalidHereafter: posixTimeToIso8601(action.results.environment.timeInterval.to)}) + txId = await lifecycle.contracts.applyInputs(contractId, {inputs: action.results.inputs}) console.log(`Input applied with txId ${txId}`) await waitIndicator(lifecycle.wallet, txId); - return contractMenu(lifecycle, contractId) + return contractMenu(lifecycle, scheme, contractId) case "return": return; } @@ -309,28 +297,171 @@ async function mainLoop(lifecycle: RuntimeLifecycle) { } // #endregion -// #region Marlowe specifics -interface DelayPaymentSchema { +// #region Delay Payment Contract +/** + * These are the parameters of the contract + */ +interface DelayPaymentScheme { + /** + * Who is making the delayed payment + */ payFrom: Address; + /** + * Who is receiving the payment + */ payTo: Address; + /** + * The amount of lovelaces to be paid + */ amount: bigint; + /** + * The deadline for the payment to be made. If the payment is not made by this date, the contract can be closed + */ depositDeadline: Date; + /** + * A date after the payment can be released to the receiver. + * NOTE: An empty transaction must be done to close the contract + */ releaseDeadline: Date; } + +function mkDelayPayment(scheme: DelayPaymentScheme): ContractBundle { + return { + main: "initial-deposit", + bundle: [ + { + label: "release-funds", + type: "contract", + value: { + when: [], + timeout: datetoTimeout(scheme.releaseDeadline), + timeout_continuation: "close", + }, + }, + { + label: "initial-deposit", + type: "contract", + value: { + when: [ + { + case: { + party: scheme.payFrom, + deposits: scheme.amount, + of_token: lovelace, + into_account: scheme.payTo, + }, + then: { + ref: "release-funds", + }, + }, + ], + timeout: datetoTimeout(scheme.depositDeadline), + timeout_continuation: "close", + }, + }, + ], + }; +} +// #endregion + +// #region Delay Payment State +/** + * The delay payment contract can be in one of the following logical states: + */ +type DelayPaymentState = + | InitialState + | PaymentDeposited + | PaymentMissed + | PaymentReady + | Closed; +/** + * In the initial state the contract is waiting for the payment to be deposited + */ +type InitialState = { + type: "InitialState"; +} + +/** + * After the payment is deposited, the contract is waiting for the payment to be released + */ +type PaymentDeposited = { + type: "PaymentDeposited"; +} + +/** + * If the payment is not deposited by the deadline, the contract can be closed. + * NOTE: It is not necesary to close the contract, as it will consume transaction fee (but it will release + * the minUTXO) + */ +type PaymentMissed = { + type: "PaymentMissed"; +} + +/** + * After the release deadline, the payment is still in the contract, and it is ready to be released. + */ +type PaymentReady = { + type: "PaymentReady"; +} + +type Closed = { + type: "Closed"; + result: "Missed deposit" | "Payment released"; +} + +function printState(state: DelayPaymentState, scheme: DelayPaymentScheme) { + switch (state.type) { + case "InitialState": + console.log(`Waiting ${scheme.payFrom.address} to deposit ${scheme.amount}`); + break; + case "PaymentDeposited": + console.log(`Payment deposited, waiting until ${scheme.releaseDeadline} to be able to release the payment`); + break; + case "PaymentMissed": + console.log(`Payment missed on ${scheme.depositDeadline}, contract can be closed to retrieve minUTXO`); + break; + case "PaymentReady": + console.log(`Payment ready to be released`); + break; + case "Closed": + console.log(`Contract closed: ${state.result}`); + break; + } +} + +function getState(scheme: DelayPaymentScheme, currentTime: Date, history: SingleInputTx[]): DelayPaymentState { + if (history.length === 0) { + if (currentTime < scheme.depositDeadline) { + return { type: "InitialState" }; + } else { + return { type: "PaymentMissed" }; + } + } else if (history.length === 1) { + // If the first transaction doesn't have an input, it means it was used to advace a timeouted contract + if (!history[0].input) { + return { type: "Closed", result: "Missed deposit" }; + } + if (currentTime < scheme.releaseDeadline) { + return { type: "PaymentDeposited" }; + } else { + return { type: "PaymentReady" }; + } + } else if (history.length === 2) { + return { type: "Closed", result: "Payment released" }; + } else { + throw new Error("Wrong state/contract, too many transactions"); + } +} + +// #endregion + // TODO: move to marlowe-object type ContractBundle = { main: Label; bundle: Bundle; }; -const splitAddress = ({ address }: Address) => { - const halfLength = Math.floor(address.length / 2); - const s1 = address.substring(0, halfLength); - const s2 = address.substring(halfLength); - return [s1, s2]; -}; - -const mkDelayPaymentTags = (schema: DelayPaymentSchema) => { +const mkDelayPaymentTags = (schema: DelayPaymentScheme) => { const tag = "DELAY_PYMNT-1"; const tags = {} as Tags; @@ -343,13 +474,38 @@ const mkDelayPaymentTags = (schema: DelayPaymentSchema) => { tags[`${tag}-release`] = schema.releaseDeadline; return tags; }; + +const extractSchemeFromTags = (tags: unknown): DelayPaymentScheme => { + const tagsGuard = t.type({ + "DELAY_PYMNT-1-from-0": t.string, + "DELAY_PYMNT-1-from-1": t.string, + "DELAY_PYMNT-1-to-0": t.string, + "DELAY_PYMNT-1-to-1": t.string, + "DELAY_PYMNT-1-amount": t.bigint, + "DELAY_PYMNT-1-deposit": t.string, + "DELAY_PYMNT-1-release": t.string, + }); + + if (!tagsGuard.is(tags)) { + throw new Error("The contract does not have the expected tags"); + } + + return { + payFrom: { address: `${tags["DELAY_PYMNT-1-from-0"]}${tags["DELAY_PYMNT-1-from-1"]}` }, + payTo: { address: `${tags["DELAY_PYMNT-1-to-0"]}${tags["DELAY_PYMNT-1-to-1"]}` }, + amount: tags["DELAY_PYMNT-1-amount"], + depositDeadline: new Date(tags["DELAY_PYMNT-1-deposit"]), + releaseDeadline: new Date(tags["DELAY_PYMNT-1-release"]), + }; +} + async function createContract( lifecycle: RuntimeLifecycle, - schema: DelayPaymentSchema + schema: DelayPaymentScheme ): Promise<[ContractId, TxId]> { const contractBundle = mkDelayPayment(schema); const tags = mkDelayPaymentTags(schema); - // TODO: create a new function in lifecycle `createContractFromBundle` + // TODO: PLT-9089: Modify runtimeLifecycle.contracts.createContract to support bundle (calling createContractSources) const contractSources = await lifecycle.restClient.createContractSources( contractBundle.main, contractBundle.bundle @@ -372,44 +528,6 @@ async function createContract( //---------------- } -function mkDelayPayment(schema: DelayPaymentSchema): ContractBundle { - return { - main: "initial-deposit", - bundle: [ - { - label: "release-funds", - type: "contract", - value: { - when: [], - timeout: datetoTimeout(schema.releaseDeadline), - timeout_continuation: "close", - }, - }, - { - label: "initial-deposit", - type: "contract", - value: { - when: [ - { - case: { - party: schema.payFrom, - deposits: schema.amount, - of_token: lovelace, - into_account: schema.payTo, - }, - then: { - ref: "release-funds", - }, - }, - ], - timeout: datetoTimeout(schema.depositDeadline), - timeout_continuation: "close", - }, - }, - ], - }; -} - async function main() { const config = await readConfig(args["--config"]); const lucid = await Lucid.new( @@ -428,4 +546,4 @@ async function main() { }); await mainLoop(lifecycle); } -// #endregion + diff --git a/packages/adapter/src/time.ts b/packages/adapter/src/time.ts index 08c64541..070c58b9 100644 --- a/packages/adapter/src/time.ts +++ b/packages/adapter/src/time.ts @@ -10,7 +10,11 @@ export const POSIXTime = t.bigint; export const posixTimeToIso8601 = (posixTime: POSIXTime): ISO8601 => datetoIso8601(new Date(Number(posixTime))); + export const datetoIso8601 = (date: Date): ISO8601 => date.toISOString(); +export const iso8601ToPosixTime = (iso8601: ISO8601): POSIXTime => + BigInt(new Date(iso8601).getTime()); + // a minute in milliseconds export const MINUTES = 1000 * 60; diff --git a/packages/language/core/v1/src/semantics.ts b/packages/language/core/v1/src/semantics.ts index b334c989..d19da3aa 100644 --- a/packages/language/core/v1/src/semantics.ts +++ b/packages/language/core/v1/src/semantics.ts @@ -91,6 +91,7 @@ import { POSIXTime } from "@marlowe.io/adapter/time"; export { Payment, Transaction, + SingleInputTx, NonPositiveDeposit, NonPositivePay, PartialPay, @@ -382,6 +383,7 @@ function giveMoney( ]; } +// TODO: Move to adapter const minBigint = (a: bigint, b: bigint): bigint => (a < b ? a : b); const maxBigint = (a: bigint, b: bigint): bigint => (a > b ? a : b); diff --git a/packages/language/core/v1/src/transaction.ts b/packages/language/core/v1/src/transaction.ts index e1adaf69..56b86fbd 100644 --- a/packages/language/core/v1/src/transaction.ts +++ b/packages/language/core/v1/src/transaction.ts @@ -39,7 +39,7 @@ export const PaymentGuard: t.Type = t.type({ }); /** - * TODO: Comment + * A marlowe transaction can consist of multiple inputs applied in a time interval * @category Transaction */ export interface Transaction { @@ -56,6 +56,21 @@ export const TransactionGuard: t.Type = t.type({ tx_inputs: t.array(InputGuard), }); +/** + * In the marlowe specification we formally prove that computing a transaction of multiple inputs + * in a time interval has the same semantics as computing multiple transactions of a single input. + * Having an array of this data structure helps the developer to reason about what inputs were applied + * to a contract, without caring if they were applied in a single transaction or in multiple ones. + * @category Transaction + */ +export interface SingleInputTx { + interval: TimeInterval; + /** If the input is undefined, the transaction was used to reduce a non quiesent contract. + This can happen to advance a timeout or if the contract doesn't start with a When + */ + input?: Input; +} + /** * TODO: Comment * @category Transaction Warning diff --git a/packages/runtime/client/rest/src/pagination.ts b/packages/runtime/client/rest/src/pagination.ts index e7a97fa8..8fa6ffea 100644 --- a/packages/runtime/client/rest/src/pagination.ts +++ b/packages/runtime/client/rest/src/pagination.ts @@ -17,7 +17,7 @@ export type ItemRange = t.TypeOf; export const itemRanged = (s: string) => unsafeEither(ItemRangeGuard.decode(s)); export interface Page { - current: ItemRange; + current?: ItemRange; next?: ItemRange; /** * Total Contracts from the query. @@ -34,9 +34,8 @@ export const PageGuard = assertGuardEqual( t.intersection([ t.type({ total: t.number, - current: ItemRangeGuard, }), - t.partial({ previous: ItemRangeGuard }), + t.partial({ current: ItemRangeGuard }), t.partial({ next: ItemRangeGuard }), ]) ); diff --git a/packages/runtime/lifecycle/src/api.ts b/packages/runtime/lifecycle/src/api.ts index 2a1e44d1..6d5c431f 100644 --- a/packages/runtime/lifecycle/src/api.ts +++ b/packages/runtime/lifecycle/src/api.ts @@ -20,6 +20,7 @@ import { 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"; export type RuntimeLifecycle = { wallet: WalletAPI; @@ -264,6 +265,12 @@ export interface ContractsAPI { * @throws Error | DecodingError */ getContractIds(): Promise; + + /** + * Get a list of the applied inputs for a given contract + * @param contractId + */ + getInputHistory(contractId: ContractId): Promise; } export type PayoutsDI = WalletDI & RestDI; diff --git a/packages/runtime/lifecycle/src/generic/contracts.ts b/packages/runtime/lifecycle/src/generic/contracts.ts index ac053747..e55fe25d 100644 --- a/packages/runtime/lifecycle/src/generic/contracts.ts +++ b/packages/runtime/lifecycle/src/generic/contracts.ts @@ -1,5 +1,6 @@ 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 { @@ -18,6 +19,7 @@ import { AddressesAndCollaterals, HexTransactionWitnessSet, transactionWitnessSetTextEnvelope, + BlockHeader, } from "@marlowe.io/runtime-core"; import { @@ -32,6 +34,8 @@ import { BuildCreateContractTxResponse, TransactionTextEnvelope, } from "@marlowe.io/runtime-rest-client/contract"; +import { SingleInputTx } from "@marlowe.io/language-core-v1/transaction.js"; +import { iso8601ToPosixTime } from "@marlowe.io/adapter/time"; export function mkContractLifecycle( wallet: WalletAPI, @@ -44,8 +48,57 @@ export function mkContractLifecycle( applyInputs: applyInputsTx(di), getApplicableInputs: getApplicableInputs(di), getContractIds: getContractIds(di), + getInputHistory: getInputHistory(di), }; } +const getInputHistory = + ({ restClient }: ContractsDI) => + async (contractId: ContractId): Promise => { + const transactionHeaders = await restClient.getTransactionsForContract( + contractId + ); + const transactions = await Promise.all( + transactionHeaders.transactions.map((txHeader) => + restClient.getContractTransactionById( + contractId, + txHeader.transactionId + ) + ) + ); + const sortOptionalBlock = ( + a: Option, + b: Option + ) => { + if (a._tag === "None" || b._tag === "None") { + // TODO: to avoid this error we should provide a higer level function that gets the transactions as the different + // status and with the appropiate values for each state. + throw new Error("A confirmed transaction should have a valid block"); + } else { + if (a.value.blockNo < b.value.blockNo) { + return -1; + } else if (a.value.blockNo > b.value.blockNo) { + return 1; + } else { + return 0; + } + } + }; + return transactions + .filter((tx) => tx.status === "confirmed") + .sort((a, b) => sortOptionalBlock(a.block, b.block)) + .map((tx) => { + const interval = { + from: iso8601ToPosixTime(tx.invalidBefore), + to: iso8601ToPosixTime(tx.invalidHereafter), + }; + if (tx.inputs.length === 0) { + return [{ interval }]; + } else { + return tx.inputs.map((input) => ({ input, interval })); + } + }) + .flat(); + }; const submitCreateTx = ({ wallet, restClient }: ContractsDI) => From 1babfaaf6091a37bfff7fab412f1abf68879f3a2 Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Fri, 5 Jan 2024 22:02:06 -0300 Subject: [PATCH 11/22] Add source maps to the integrated debugging terminal --- .vscode/settings.json | 17 ++++++++++++++++- tsconfig-base.json | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 25fa6215..14c3a934 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,18 @@ { - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "debug.javascript.terminalOptions": { + "outFiles": [ + "${workspaceFolder}/examples/nodejs/dist/**/*.js", + "${workspaceFolder}/packages/adapter/dist/esm/**/*.js", + "${workspaceFolder}/packages/language/core/v1/dist/esm/**/*.js", + "${workspaceFolder}/packages/language/examples/dist/esm/**/*.js", + "${workspaceFolder}/packages/language/specification-client/dist/esm/**/*.js", + "${workspaceFolder}/packages/token-metadata-client/dist/esm/**/*.js", + "${workspaceFolder}/packages/wallet/dist/esm/**/*.js", + "${workspaceFolder}/packages/runtime/client/rest/dist/esm/**/*.js", + "${workspaceFolder}/packages/runtime/core/dist/esm/**/*.js", + "${workspaceFolder}/packages/runtime/lifecycle/dist/esm/**/*.js", + "${workspaceFolder}/packages/marlowe-object/dist/esm/**/*.js" + ] + } } diff --git a/tsconfig-base.json b/tsconfig-base.json index 14ec5ef0..ea80b9e0 100644 --- a/tsconfig-base.json +++ b/tsconfig-base.json @@ -5,6 +5,7 @@ "declaration": true, "declarationMap": true, "esModuleInterop": true, + "sourceMap": true, "inlineSourceMap": false, "lib": ["es2020", "dom"], "target": "ES2020", From d5135d90853ed46003b3835d50be64647dda90c0 Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Mon, 8 Jan 2024 12:47:29 -0300 Subject: [PATCH 12/22] Add staking address to the delay payment --- examples/nodejs/src/marlowe-object-flow.ts | 37 +++++++++++++--------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/examples/nodejs/src/marlowe-object-flow.ts b/examples/nodejs/src/marlowe-object-flow.ts index 50a8d411..3d726662 100644 --- a/examples/nodejs/src/marlowe-object-flow.ts +++ b/examples/nodejs/src/marlowe-object-flow.ts @@ -10,13 +10,13 @@ import { Lucid, Blockfrost, C } from "lucid-cardano"; import { readConfig } from "./config.js"; import { datetoTimeout, - getNextTimeout, - Timeout, } from "@marlowe.io/language-core-v1"; import { contractId, ContractId, contractIdToTxId, + stakeAddressBech32, + StakeAddressBech32, Tags, transactionWitnessSetTextEnvelope, TxId, @@ -25,20 +25,17 @@ import { Address } from "@marlowe.io/language-core-v1"; 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 { - ApplicableAction, AppliedActionResult, getApplicableActions, mkApplicableActionsFilter, } from "./experimental-features/applicable-inputs.js"; import arg from "arg"; -import { POSIXTime, posixTimeToIso8601 } from "@marlowe.io/adapter/time"; import { splitAddress } from "./experimental-features/metadata.js"; import { SingleInputTx } from "../../../packages/language/core/v1/dist/esm/transaction.js"; import * as t from "io-ts/lib/index.js" import { deepEqual } from "@marlowe.io/adapter/deep-equal"; + const args = arg({ "--help": Boolean, "--config": String, @@ -94,6 +91,7 @@ function bech32Validator(value: string) { return "Invalid address"; } } + function positiveBigIntValidator(value: string) { try { if (BigInt(value) > 0) { @@ -117,7 +115,7 @@ function dateInFutureValidator(value: string) { return true; } -async function createContractMenu(lifecycle: RuntimeLifecycle) { +async function createContractMenu(lifecycle: RuntimeLifecycle, rewardAddress?: StakeAddressBech32) { const payee = await input({ message: "Enter the payee address", validate: bech32Validator, @@ -143,11 +141,14 @@ async function createContractMenu(lifecycle: RuntimeLifecycle) { const walletAddress = await lifecycle.wallet.getChangeAddress(); console.log( - `Making a delayed payment from ${walletAddress} to ${payee} for ${amount} lovelaces` + `Making a delayed payment:\n * from ${walletAddress}\n * to ${payee}\n * for ${amount} lovelaces\n` ); console.log( - `The payment must be deposited by ${depositDeadline} and will be released to ${payee} by ${releaseDeadline}` + `The payment must be deposited before ${depositDeadline} and can be released to the payee after ${releaseDeadline}` ); + if (rewardAddress) { + console.log(`In the meantime, the contract will stake rewards to ${rewardAddress}`); + } const scheme = { payFrom: { address: walletAddress }, @@ -156,7 +157,7 @@ async function createContractMenu(lifecycle: RuntimeLifecycle) { depositDeadline, releaseDeadline, }; - const [contractId, txId] = await createContract(lifecycle, scheme); + const [contractId, txId] = await createContract(lifecycle, scheme, rewardAddress); console.log(`Contract created with id ${contractId}`); @@ -265,9 +266,11 @@ async function contractMenu( } } -async function mainLoop(lifecycle: RuntimeLifecycle) { +async function mainLoop(lifecycle: RuntimeLifecycle, rewardAddress?: StakeAddressBech32) { try { while (true) { + const address = await lifecycle.wallet.getChangeAddress(); + console.log("Wallet address:", address); const action = await select({ message: "Main menu", choices: [ @@ -278,7 +281,7 @@ async function mainLoop(lifecycle: RuntimeLifecycle) { }); switch (action) { case "create": - await createContractMenu(lifecycle); + await createContractMenu(lifecycle, rewardAddress); break; case "load": await loadContractMenu(lifecycle); @@ -501,7 +504,9 @@ const extractSchemeFromTags = (tags: unknown): DelayPaymentScheme => { async function createContract( lifecycle: RuntimeLifecycle, - schema: DelayPaymentScheme + schema: DelayPaymentScheme, + rewardAddress?: StakeAddressBech32 + ): Promise<[ContractId, TxId]> { const contractBundle = mkDelayPayment(schema); const tags = mkDelayPaymentTags(schema); @@ -515,6 +520,7 @@ async function createContract( sourceId: contractSources.contractSourceId, tags, changeAddress: walletAddress, + stakeAddress: rewardAddress, minimumLovelaceUTxODeposit: 3_000_000, version: "v1", }); @@ -535,7 +541,8 @@ async function main() { config.network ); lucid.selectWalletFromSeed(config.seedPhrase); - + const rewardAddressStr = await lucid.wallet.rewardAddress(); + const rewardAddress = rewardAddressStr ? stakeAddressBech32(rewardAddressStr) : undefined; const runtimeURL = config.runtimeURL; const wallet = mkLucidWallet(lucid); @@ -544,6 +551,6 @@ async function main() { runtimeURL, wallet, }); - await mainLoop(lifecycle); + await mainLoop(lifecycle, rewardAddress); } From 72083ec3d15f29c54cb4ea8d4844b1d18fcc40f0 Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Mon, 8 Jan 2024 16:35:49 -0300 Subject: [PATCH 13/22] Add comments --- examples/nodejs/src/marlowe-object-flow.ts | 181 +++++++++++++++------ 1 file changed, 128 insertions(+), 53 deletions(-) diff --git a/examples/nodejs/src/marlowe-object-flow.ts b/examples/nodejs/src/marlowe-object-flow.ts index 3d726662..43b1a734 100644 --- a/examples/nodejs/src/marlowe-object-flow.ts +++ b/examples/nodejs/src/marlowe-object-flow.ts @@ -36,34 +36,46 @@ import { SingleInputTx } from "../../../packages/language/core/v1/dist/esm/trans import * as t from "io-ts/lib/index.js" import { deepEqual } from "@marlowe.io/adapter/deep-equal"; -const args = arg({ - "--help": Boolean, - "--config": String, - "-c": "--config", -}); - -if (args["--help"]) { - printHelp(0); -} +// When this script is called, start with main. main(); -// #region Interactive menu -function printHelp(exitStatus: number): never { - console.log( - "Usage: npm run marlowe-object-flow -- --config " - ); - console.log(""); - console.log("Example:"); - console.log( - " npm run marlowe-object-flow -- --config alice.config" - ); - console.log("Options:"); - console.log(" --help: Print this message"); - console.log(" --config | -c: The path to the config file [default .config.json]"); - process.exit(exitStatus); +// #region Command line arguments +function parseCli() { + const args = arg({ + "--help": Boolean, + "--config": String, + "-c": "--config", + }); + + if (args["--help"]) { + printHelp(0); + } + function printHelp(exitStatus: number): never { + console.log( + "Usage: npm run marlowe-object-flow -- --config " + ); + console.log(""); + console.log("Example:"); + console.log( + " npm run marlowe-object-flow -- --config alice.config" + ); + console.log("Options:"); + console.log(" --help: Print this message"); + console.log(" --config | -c: The path to the config file [default .config.json]"); + process.exit(exitStatus); + } + return args; } +// #endregion + +// #region Interactive menu + +/** + * Small command line utility that prints a confirmation message and writes dots until the transaction is confirmed + * NOTE: If we make more node.js cli tools, we should move this to a common place + */ async function waitIndicator(wallet: WalletAPI, txId: TxId) { process.stdout.write("Waiting for the transaction to be confirmed..."); let done = false; @@ -83,6 +95,10 @@ async function waitIndicator(wallet: WalletAPI, txId: TxId) { process.stdout.write("\n"); } +/** + * This is an Inquirer.js validator for bech32 addresses + * @returns true if the address is valid, or a string with the error message otherwise + */ function bech32Validator(value: string) { try { C.Address.from_bech32(value); @@ -92,6 +108,10 @@ function bech32Validator(value: string) { } } +/** + * This is an Inquirer.js validator for positive bigints + * @returns true if the value is a positive bigint, or a string with the error message otherwise + */ function positiveBigIntValidator(value: string) { try { if (BigInt(value) > 0) { @@ -104,6 +124,10 @@ function positiveBigIntValidator(value: string) { } } +/** + * This is an Inquirer.js validator for dates in the future + * @returns true if the value is a date in the future, or a string with the error message otherwise + */ function dateInFutureValidator(value: string) { const d = new Date(value); if (isNaN(d.getTime())) { @@ -115,6 +139,11 @@ function dateInFutureValidator(value: string) { return true; } +/** + * This is an Inquirer.js flow to create a contract + * @param lifecycle An instance of the RuntimeLifecycle + * @param rewardAddress An optional reward address to stake the contract rewards + */ async function createContractMenu(lifecycle: RuntimeLifecycle, rewardAddress?: StakeAddressBech32) { const payee = await input({ message: "Enter the payee address", @@ -163,20 +192,33 @@ async function createContractMenu(lifecycle: RuntimeLifecycle, rewardAddress?: S await waitIndicator(lifecycle.wallet, txId); - await contractMenu(lifecycle, scheme, contractId); + return contractMenu(lifecycle, scheme, contractId); } +/** + * This is an Inquirer.js flow to load an existing contract + * @param lifecycle + * @returns + */ async function loadContractMenu(lifecycle: RuntimeLifecycle) { + // First we ask the user for a contract id const cidStr = await input({ message: "Enter the contractId", }); const cid = contractId(cidStr); - const contractDetails = await lifecycle.restClient.getContractById( - cid - ); + // Then we make sure that contract id is an instance of our delayed payment contract + const scheme = await validateExistingContract(lifecycle, cid); + if (scheme === "InvalidTags") { + console.log("Invalid contract, it does not have the expected tags"); + return; + } + if (scheme === "InvalidContract") { + console.log("Invalid contract, it does not have the expected contract source"); + return; + } - const scheme = extractSchemeFromTags(contractDetails.tags); + // If it is, we print the contract details and go to the contract menu console.log("Contract details:"); console.log(` * Pay from: ${scheme.payFrom.address}`); console.log(` * Pay to: ${scheme.payTo.address}`); @@ -184,36 +226,29 @@ async function loadContractMenu(lifecycle: RuntimeLifecycle) { console.log(` * Deposit deadline: ${scheme.depositDeadline}`); console.log(` * Release deadline: ${scheme.releaseDeadline}`); - const contractBundle = mkDelayPayment(scheme); - const {contractSourceId} = await lifecycle.restClient.createContractSources( - contractBundle.main, - contractBundle.bundle - ); - const initialContract = await lifecycle.restClient.getContractSourceById({ contractSourceId} ); - - if (!deepEqual(initialContract, contractDetails.initialContract)) { - throw new Error("The contract on chain does not match the expected contract for the scheme"); - }; - return contractMenu(lifecycle, scheme, cid); } +/** + * This is an Inquirer.js flow to interact with a contract + */ async function contractMenu( lifecycle: RuntimeLifecycle, scheme: DelayPaymentScheme, contractId: ContractId ): Promise { + // Get and print the contract logical state. const inputHistory = await lifecycle.contracts.getInputHistory(contractId); - const contractState = getState(scheme, new Date(), inputHistory); printState(contractState, scheme); + if (contractState.type === "Closed") return; + // See what actions are applicable to the current contract state const applicableActions = await getApplicableActions( lifecycle.restClient, contractId ); - const myActionsFilter = await mkApplicableActionsFilter(lifecycle.wallet); const myActions = applicableActions.filter(myActionsFilter) @@ -242,22 +277,14 @@ async function contractMenu( message: "Contract menu", choices, }); - let txId switch (action.actionType) { case "check-state": return contractMenu(lifecycle, scheme, contractId) case "advance": - if (!action.results) throw new Error("This should not happen") - console.log("Advancing contract"); - txId = await lifecycle.contracts.applyInputs(contractId, {inputs: action.results.inputs}) - console.log(`Input applied with txId ${txId}`) - await waitIndicator(lifecycle.wallet, txId); - return contractMenu(lifecycle, scheme, contractId) case "deposit": - // TODO: Remove duplication if (!action.results) throw new Error("This should not happen") - console.log("Making deposit"); - txId = await lifecycle.contracts.applyInputs(contractId, {inputs: action.results.inputs}) + console.log("Applying input"); + const txId = await lifecycle.contracts.applyInputs(contractId, {inputs: action.results.inputs}) console.log(`Input applied with txId ${txId}`) await waitIndicator(lifecycle.wallet, txId); return contractMenu(lifecycle, scheme, contractId) @@ -478,7 +505,7 @@ const mkDelayPaymentTags = (schema: DelayPaymentScheme) => { return tags; }; -const extractSchemeFromTags = (tags: unknown): DelayPaymentScheme => { +const extractSchemeFromTags = (tags: unknown): DelayPaymentScheme | undefined => { const tagsGuard = t.type({ "DELAY_PYMNT-1-from-0": t.string, "DELAY_PYMNT-1-from-1": t.string, @@ -490,7 +517,7 @@ const extractSchemeFromTags = (tags: unknown): DelayPaymentScheme => { }); if (!tagsGuard.is(tags)) { - throw new Error("The contract does not have the expected tags"); + return; } return { @@ -534,7 +561,55 @@ async function createContract( //---------------- } +type ValidationResults = 'InvalidTags' | 'InvalidContract' | DelayPaymentScheme + +/** + * This function checks if the contract with the given id is an instance of the delay payment contract + * @param lifecycle + * @param contractId + * @returns + */ +async function validateExistingContract( + lifecycle: RuntimeLifecycle, + contractId: ContractId, +): Promise { + // First we try to fetch the contract details and the required tags + const contractDetails = await lifecycle.restClient.getContractById( + contractId + ); + + const scheme = extractSchemeFromTags(contractDetails.tags); + + if (!scheme) { + return 'InvalidTags'; + } + + // If the contract seems to be an instance of the contract we want (meanin, we were able + // to retrieve the contract scheme) we check that the actual initial contract has the same + // sources. + // This has 2 purposes: + // 1. Make sure we are interacting with the expected contract + // 2. Share the same sources between different Runtimes. + // When a contract source is uploaded to the runtime, it merkleizes the source code, + // but it doesn't share those sources with other runtime instances. One option would be + // to download the sources from the initial runtime and share those with another runtime. + // Or this option which doesn't require runtime to runtime communication, and just requires + // the dapp to be able to recreate the same sources. + const contractBundle = mkDelayPayment(scheme); + const {contractSourceId} = await lifecycle.restClient.createContractSources( + contractBundle.main, + contractBundle.bundle + ); + const initialContract = await lifecycle.restClient.getContractSourceById({ contractSourceId} ); + + if (!deepEqual(initialContract, contractDetails.initialContract)) { + return "InvalidContract"; + }; + return scheme; +} + async function main() { + const args = parseCli(); const config = await readConfig(args["--config"]); const lucid = await Lucid.new( new Blockfrost(config.blockfrostUrl, config.blockfrostProjectId), From c5c3219550848a9a7a070752109eb2225c2c411e Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Mon, 8 Jan 2024 16:44:26 -0300 Subject: [PATCH 14/22] Add ContractBundle to marlowe-object --- examples/nodejs/src/marlowe-object-flow.ts | 171 +++++++++++++-------- packages/marlowe-object/src/guards.ts | 1 + packages/marlowe-object/src/index.ts | 1 + packages/marlowe-object/src/object.ts | 17 ++ packages/runtime/client/rest/src/index.ts | 12 +- 5 files changed, 127 insertions(+), 75 deletions(-) diff --git a/examples/nodejs/src/marlowe-object-flow.ts b/examples/nodejs/src/marlowe-object-flow.ts index 43b1a734..6d3f2737 100644 --- a/examples/nodejs/src/marlowe-object-flow.ts +++ b/examples/nodejs/src/marlowe-object-flow.ts @@ -8,9 +8,7 @@ import { mkLucidWallet, WalletAPI } from "@marlowe.io/wallet"; import { mkRuntimeLifecycle } from "@marlowe.io/runtime-lifecycle"; import { Lucid, Blockfrost, C } from "lucid-cardano"; import { readConfig } from "./config.js"; -import { - datetoTimeout, -} from "@marlowe.io/language-core-v1"; +import { datetoTimeout } from "@marlowe.io/language-core-v1"; import { contractId, ContractId, @@ -22,7 +20,7 @@ import { TxId, } from "@marlowe.io/runtime-core"; import { Address } from "@marlowe.io/language-core-v1"; -import { Bundle, Label, lovelace } from "@marlowe.io/marlowe-object"; +import { ContractBundle, lovelace } from "@marlowe.io/marlowe-object"; import { input, select } from "@inquirer/prompts"; import { RuntimeLifecycle } from "@marlowe.io/runtime-lifecycle/api"; import { @@ -33,7 +31,7 @@ import { import arg from "arg"; import { splitAddress } from "./experimental-features/metadata.js"; import { SingleInputTx } from "../../../packages/language/core/v1/dist/esm/transaction.js"; -import * as t from "io-ts/lib/index.js" +import * as t from "io-ts/lib/index.js"; import { deepEqual } from "@marlowe.io/adapter/deep-equal"; // When this script is called, start with main. @@ -51,17 +49,15 @@ function parseCli() { printHelp(0); } function printHelp(exitStatus: number): never { - console.log( - "Usage: npm run marlowe-object-flow -- --config " - ); + console.log("Usage: npm run marlowe-object-flow -- --config "); console.log(""); console.log("Example:"); - console.log( - " npm run marlowe-object-flow -- --config alice.config" - ); + console.log(" npm run marlowe-object-flow -- --config alice.config"); console.log("Options:"); console.log(" --help: Print this message"); - console.log(" --config | -c: The path to the config file [default .config.json]"); + console.log( + " --config | -c: The path to the config file [default .config.json]" + ); process.exit(exitStatus); } return args; @@ -69,7 +65,6 @@ function parseCli() { // #endregion - // #region Interactive menu /** @@ -144,7 +139,10 @@ function dateInFutureValidator(value: string) { * @param lifecycle An instance of the RuntimeLifecycle * @param rewardAddress An optional reward address to stake the contract rewards */ -async function createContractMenu(lifecycle: RuntimeLifecycle, rewardAddress?: StakeAddressBech32) { +async function createContractMenu( + lifecycle: RuntimeLifecycle, + rewardAddress?: StakeAddressBech32 +) { const payee = await input({ message: "Enter the payee address", validate: bech32Validator, @@ -176,7 +174,9 @@ async function createContractMenu(lifecycle: RuntimeLifecycle, rewardAddress?: S `The payment must be deposited before ${depositDeadline} and can be released to the payee after ${releaseDeadline}` ); if (rewardAddress) { - console.log(`In the meantime, the contract will stake rewards to ${rewardAddress}`); + console.log( + `In the meantime, the contract will stake rewards to ${rewardAddress}` + ); } const scheme = { @@ -186,7 +186,11 @@ async function createContractMenu(lifecycle: RuntimeLifecycle, rewardAddress?: S depositDeadline, releaseDeadline, }; - const [contractId, txId] = await createContract(lifecycle, scheme, rewardAddress); + const [contractId, txId] = await createContract( + lifecycle, + scheme, + rewardAddress + ); console.log(`Contract created with id ${contractId}`); @@ -214,7 +218,9 @@ async function loadContractMenu(lifecycle: RuntimeLifecycle) { return; } if (scheme === "InvalidContract") { - console.log("Invalid contract, it does not have the expected contract source"); + console.log( + "Invalid contract, it does not have the expected contract source" + ); return; } @@ -250,28 +256,42 @@ async function contractMenu( contractId ); const myActionsFilter = await mkApplicableActionsFilter(lifecycle.wallet); - const myActions = applicableActions.filter(myActionsFilter) - - const choices: Array<{name: string, value: {actionType: string, results?: AppliedActionResult}}> = [ - { name: "Re-check contract state", value: {actionType: "check-state", results: undefined} }, - ...myActions.map(action => { + const myActions = applicableActions.filter(myActionsFilter); + + const choices: Array<{ + name: string; + value: { actionType: string; results?: AppliedActionResult }; + }> = [ + { + name: "Re-check contract state", + value: { actionType: "check-state", results: undefined }, + }, + ...myActions.map((action) => { switch (action.type) { case "Advance": - return { name: "Close contract", - description: contractState.type == "PaymentMissed" ? "The payer will receive minUTXO" : "The payer will receive minUTXO and the payee will receive the payment", - value: {actionType: "advance", results: action.applyAction() } - } + description: + contractState.type == "PaymentMissed" + ? "The payer will receive minUTXO" + : "The payer will receive minUTXO and the payee will receive the payment", + value: { actionType: "advance", results: action.applyAction() }, + }; case "Deposit": - return { name: `Deposit ${action.deposit.deposits} lovelaces`, value: {actionType: "deposit", results: action.applyAction()} } + return { + name: `Deposit ${action.deposit.deposits} lovelaces`, + value: { actionType: "deposit", results: action.applyAction() }, + }; default: - throw new Error("Unexpected action type") + throw new Error("Unexpected action type"); } }), - { name: "Return to main menu", value: {actionType: "return", results: undefined} }, - ] + { + name: "Return to main menu", + value: { actionType: "return", results: undefined }, + }, + ]; const action = await select({ message: "Contract menu", @@ -279,21 +299,26 @@ async function contractMenu( }); switch (action.actionType) { case "check-state": - return contractMenu(lifecycle, scheme, contractId) + return contractMenu(lifecycle, scheme, contractId); case "advance": case "deposit": - if (!action.results) throw new Error("This should not happen") + if (!action.results) throw new Error("This should not happen"); console.log("Applying input"); - const txId = await lifecycle.contracts.applyInputs(contractId, {inputs: action.results.inputs}) - console.log(`Input applied with txId ${txId}`) + const txId = await lifecycle.contracts.applyInputs(contractId, { + inputs: action.results.inputs, + }); + console.log(`Input applied with txId ${txId}`); await waitIndicator(lifecycle.wallet, txId); - return contractMenu(lifecycle, scheme, contractId) + return contractMenu(lifecycle, scheme, contractId); case "return": return; } } -async function mainLoop(lifecycle: RuntimeLifecycle, rewardAddress?: StakeAddressBech32) { +async function mainLoop( + lifecycle: RuntimeLifecycle, + rewardAddress?: StakeAddressBech32 +) { try { while (true) { const address = await lifecycle.wallet.getChangeAddress(); @@ -409,14 +434,14 @@ type DelayPaymentState = */ type InitialState = { type: "InitialState"; -} +}; /** * After the payment is deposited, the contract is waiting for the payment to be released */ type PaymentDeposited = { type: "PaymentDeposited"; -} +}; /** * If the payment is not deposited by the deadline, the contract can be closed. @@ -425,30 +450,36 @@ type PaymentDeposited = { */ type PaymentMissed = { type: "PaymentMissed"; -} +}; /** * After the release deadline, the payment is still in the contract, and it is ready to be released. */ type PaymentReady = { type: "PaymentReady"; -} +}; type Closed = { type: "Closed"; result: "Missed deposit" | "Payment released"; -} +}; function printState(state: DelayPaymentState, scheme: DelayPaymentScheme) { switch (state.type) { case "InitialState": - console.log(`Waiting ${scheme.payFrom.address} to deposit ${scheme.amount}`); + console.log( + `Waiting ${scheme.payFrom.address} to deposit ${scheme.amount}` + ); break; case "PaymentDeposited": - console.log(`Payment deposited, waiting until ${scheme.releaseDeadline} to be able to release the payment`); + console.log( + `Payment deposited, waiting until ${scheme.releaseDeadline} to be able to release the payment` + ); break; case "PaymentMissed": - console.log(`Payment missed on ${scheme.depositDeadline}, contract can be closed to retrieve minUTXO`); + console.log( + `Payment missed on ${scheme.depositDeadline}, contract can be closed to retrieve minUTXO` + ); break; case "PaymentReady": console.log(`Payment ready to be released`); @@ -459,7 +490,11 @@ function printState(state: DelayPaymentState, scheme: DelayPaymentScheme) { } } -function getState(scheme: DelayPaymentScheme, currentTime: Date, history: SingleInputTx[]): DelayPaymentState { +function getState( + scheme: DelayPaymentScheme, + currentTime: Date, + history: SingleInputTx[] +): DelayPaymentState { if (history.length === 0) { if (currentTime < scheme.depositDeadline) { return { type: "InitialState" }; @@ -485,12 +520,6 @@ function getState(scheme: DelayPaymentScheme, currentTime: Date, history: Single // #endregion -// TODO: move to marlowe-object -type ContractBundle = { - main: Label; - bundle: Bundle; -}; - const mkDelayPaymentTags = (schema: DelayPaymentScheme) => { const tag = "DELAY_PYMNT-1"; const tags = {} as Tags; @@ -505,7 +534,9 @@ const mkDelayPaymentTags = (schema: DelayPaymentScheme) => { return tags; }; -const extractSchemeFromTags = (tags: unknown): DelayPaymentScheme | undefined => { +const extractSchemeFromTags = ( + tags: unknown +): DelayPaymentScheme | undefined => { const tagsGuard = t.type({ "DELAY_PYMNT-1-from-0": t.string, "DELAY_PYMNT-1-from-1": t.string, @@ -521,26 +552,28 @@ const extractSchemeFromTags = (tags: unknown): DelayPaymentScheme | undefined => } return { - payFrom: { address: `${tags["DELAY_PYMNT-1-from-0"]}${tags["DELAY_PYMNT-1-from-1"]}` }, - payTo: { address: `${tags["DELAY_PYMNT-1-to-0"]}${tags["DELAY_PYMNT-1-to-1"]}` }, + payFrom: { + address: `${tags["DELAY_PYMNT-1-from-0"]}${tags["DELAY_PYMNT-1-from-1"]}`, + }, + payTo: { + address: `${tags["DELAY_PYMNT-1-to-0"]}${tags["DELAY_PYMNT-1-to-1"]}`, + }, amount: tags["DELAY_PYMNT-1-amount"], depositDeadline: new Date(tags["DELAY_PYMNT-1-deposit"]), releaseDeadline: new Date(tags["DELAY_PYMNT-1-release"]), }; -} +}; async function createContract( lifecycle: RuntimeLifecycle, schema: DelayPaymentScheme, rewardAddress?: StakeAddressBech32 - ): Promise<[ContractId, TxId]> { const contractBundle = mkDelayPayment(schema); const tags = mkDelayPaymentTags(schema); // TODO: PLT-9089: Modify runtimeLifecycle.contracts.createContract to support bundle (calling createContractSources) const contractSources = await lifecycle.restClient.createContractSources( - contractBundle.main, - contractBundle.bundle + contractBundle ); const walletAddress = await lifecycle.wallet.getChangeAddress(); const unsignedTx = await lifecycle.restClient.buildCreateContractTx({ @@ -561,7 +594,7 @@ async function createContract( //---------------- } -type ValidationResults = 'InvalidTags' | 'InvalidContract' | DelayPaymentScheme +type ValidationResults = "InvalidTags" | "InvalidContract" | DelayPaymentScheme; /** * This function checks if the contract with the given id is an instance of the delay payment contract @@ -571,7 +604,7 @@ type ValidationResults = 'InvalidTags' | 'InvalidContract' | DelayPaymentScheme */ async function validateExistingContract( lifecycle: RuntimeLifecycle, - contractId: ContractId, + contractId: ContractId ): Promise { // First we try to fetch the contract details and the required tags const contractDetails = await lifecycle.restClient.getContractById( @@ -581,7 +614,7 @@ async function validateExistingContract( const scheme = extractSchemeFromTags(contractDetails.tags); if (!scheme) { - return 'InvalidTags'; + return "InvalidTags"; } // If the contract seems to be an instance of the contract we want (meanin, we were able @@ -596,15 +629,16 @@ async function validateExistingContract( // Or this option which doesn't require runtime to runtime communication, and just requires // the dapp to be able to recreate the same sources. const contractBundle = mkDelayPayment(scheme); - const {contractSourceId} = await lifecycle.restClient.createContractSources( - contractBundle.main, - contractBundle.bundle + const { contractSourceId } = await lifecycle.restClient.createContractSources( + contractBundle ); - const initialContract = await lifecycle.restClient.getContractSourceById({ contractSourceId} ); + const initialContract = await lifecycle.restClient.getContractSourceById({ + contractSourceId, + }); if (!deepEqual(initialContract, contractDetails.initialContract)) { return "InvalidContract"; - }; + } return scheme; } @@ -617,7 +651,9 @@ async function main() { ); lucid.selectWalletFromSeed(config.seedPhrase); const rewardAddressStr = await lucid.wallet.rewardAddress(); - const rewardAddress = rewardAddressStr ? stakeAddressBech32(rewardAddressStr) : undefined; + const rewardAddress = rewardAddressStr + ? stakeAddressBech32(rewardAddressStr) + : undefined; const runtimeURL = config.runtimeURL; const wallet = mkLucidWallet(lucid); @@ -628,4 +664,3 @@ async function main() { }); await mainLoop(lifecycle, rewardAddress); } - diff --git a/packages/marlowe-object/src/guards.ts b/packages/marlowe-object/src/guards.ts index f7edf378..383ff773 100644 --- a/packages/marlowe-object/src/guards.ts +++ b/packages/marlowe-object/src/guards.ts @@ -84,6 +84,7 @@ export { } from "./contract.js"; export { BundleGuard as Bundle, + ContractBundleGuard as ContractBundle, ObjectPartyGuard as ObjectParty, ObjectValueGuard as ObjectValue, ObjectObservationGuard as ObjectObservation, diff --git a/packages/marlowe-object/src/index.ts b/packages/marlowe-object/src/index.ts index a2cc29f8..5669bed2 100644 --- a/packages/marlowe-object/src/index.ts +++ b/packages/marlowe-object/src/index.ts @@ -50,6 +50,7 @@ export { } from "./contract.js"; export { ObjectType, + ContractBundle, Bundle, ObjectParty, ObjectValue, diff --git a/packages/marlowe-object/src/object.ts b/packages/marlowe-object/src/object.ts index edcf0362..2c2c59eb 100644 --- a/packages/marlowe-object/src/object.ts +++ b/packages/marlowe-object/src/object.ts @@ -167,3 +167,20 @@ export type Bundle = ObjectType[]; * @category Object */ export const BundleGuard = t.array(ObjectTypeGuard); + +/** + * A contract bundle is just a {@link Bundle} with a main entrypoint. + * @category Object + */ +export interface ContractBundle { + main: Label; + bundle: Bundle; +} + +/** + * {@link !io-ts-usage | Dynamic type guard} for the {@link ContractBundle | contract bundle type}. + */ +export const ContractBundleGuard: t.Type = t.type({ + main: LabelGuard, + bundle: BundleGuard, +}); diff --git a/packages/runtime/client/rest/src/index.ts b/packages/runtime/client/rest/src/index.ts index 775db7d2..effc67d7 100644 --- a/packages/runtime/client/rest/src/index.ts +++ b/packages/runtime/client/rest/src/index.ts @@ -15,7 +15,7 @@ import { pipe } from "fp-ts/lib/function.js"; import { MarloweJSONCodec } from "@marlowe.io/adapter/codec"; import * as HTTP from "@marlowe.io/adapter/http"; -import { Bundle, Label } from "@marlowe.io/marlowe-object"; +import { ContractBundle } from "@marlowe.io/marlowe-object"; import * as Payouts from "./payout/endpoints/collection.js"; import * as Payout from "./payout/endpoints/singleton.js"; @@ -87,12 +87,10 @@ export interface RestClient { /** * Uploads a marlowe-object bundle to the runtime, giving back the hash of the main contract and the hashes of the intermediate objects. - * @param mainId A label that corresponds to the main entrypoint of the contract - * @param bundle A list of object types that are referenced by the main contract + * @param bundle Contains a list of object types and a main contract reference */ createContractSources( - mainId: Label, - bundle: Bundle + bundle: ContractBundle ): Promise; /** @@ -315,8 +313,8 @@ export function mkRestClient(baseURL: string): RestClient { ) ); }, - createContractSources(mainId, bundle) { - return Sources.createContractSources(axiosInstance)(mainId, bundle); + createContractSources({main, bundle}) { + return Sources.createContractSources(axiosInstance)(main, bundle); }, getContractSourceById(request) { return Sources.getContractSourceById(axiosInstance)(request); From 5cf68ff8953aec1d6ab927bae0cfd621ac98e23d Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Mon, 8 Jan 2024 16:51:29 -0300 Subject: [PATCH 15/22] Move max and min bigint to adapter --- .../src/experimental-features/applicable-inputs.ts | 5 ++--- jsdelivr-npm-importmap.js | 2 ++ packages/adapter/package.json | 6 ++++++ packages/adapter/src/bigint.ts | 6 ++++++ packages/language/core/v1/src/semantics.ts | 9 +++------ 5 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 packages/adapter/src/bigint.ts diff --git a/examples/nodejs/src/experimental-features/applicable-inputs.ts b/examples/nodejs/src/experimental-features/applicable-inputs.ts index 92def106..b188d9a1 100644 --- a/examples/nodejs/src/experimental-features/applicable-inputs.ts +++ b/examples/nodejs/src/experimental-features/applicable-inputs.ts @@ -40,6 +40,7 @@ import { import { AddressBech32, ContractId, PolicyId } from "@marlowe.io/runtime-core"; import { RestClient } from "@marlowe.io/runtime-rest-client"; import { WalletAPI } from "@marlowe.io/wallet"; +import * as Big from "@marlowe.io/adapter/bigint"; import { Monoid } from "fp-ts/lib/Monoid.js"; import * as R from "fp-ts/lib/Record.js"; type ActionApplicant = Party | "anybody"; @@ -318,8 +319,6 @@ const accumulatorFromNotify = (action: CanNotify) => { }; }; // TODO: Move to adapter -const minBigint = (a: bigint, b: bigint): bigint => (a < b ? a : b); -const maxBigint = (a: bigint, b: bigint): bigint => (a > b ? a : b); function mergeBounds(bounds: Bound[]): Bound[] { const mergedBounds: Bound[] = []; @@ -333,7 +332,7 @@ function mergeBounds(bounds: Bound[]): Bound[] { currentBound = {...bound}; } else { if (bound.from <= currentBound.to) { - currentBound.to = maxBigint(currentBound.to, bound.to); + currentBound.to = Big.max(currentBound.to, bound.to); } else { mergedBounds.push(currentBound); currentBound = {...bound}; diff --git a/jsdelivr-npm-importmap.js b/jsdelivr-npm-importmap.js index 54b79e36..3ad1d631 100644 --- a/jsdelivr-npm-importmap.js +++ b/jsdelivr-npm-importmap.js @@ -4,6 +4,8 @@ const importMap = { "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/adapter.js", "@marlowe.io/adapter/assoc-map": "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/assoc-map.js", + "@marlowe.io/adapter/bigint": + "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/bigint.js", "@marlowe.io/adapter/codec": "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/codec.js", "@marlowe.io/adapter/deep-equal": diff --git a/packages/adapter/package.json b/packages/adapter/package.json index 95acf1c3..f6eeba52 100644 --- a/packages/adapter/package.json +++ b/packages/adapter/package.json @@ -66,7 +66,13 @@ "import": "./dist/esm/deep-equal.js", "require": "./dist/bundled/cjs/deep-equal.cjs", "types": "./dist/esm/deep-equal.d.ts" + }, + "./bigint": { + "import": "./dist/esm/bigint.js", + "require": "./dist/bundled/cjs/bigint.cjs", + "types": "./dist/esm/bigint.d.ts" } + }, "dependencies": { "date-fns": "2.29.3", diff --git a/packages/adapter/src/bigint.ts b/packages/adapter/src/bigint.ts new file mode 100644 index 00000000..691c2118 --- /dev/null +++ b/packages/adapter/src/bigint.ts @@ -0,0 +1,6 @@ +/** + * Utility functions for bigint. + */ + +export const min = (a: bigint, b: bigint): bigint => (a < b ? a : b); +export const max = (a: bigint, b: bigint): bigint => (a > b ? a : b); diff --git a/packages/language/core/v1/src/semantics.ts b/packages/language/core/v1/src/semantics.ts index d19da3aa..b9bf9ad4 100644 --- a/packages/language/core/v1/src/semantics.ts +++ b/packages/language/core/v1/src/semantics.ts @@ -87,6 +87,7 @@ import { } from "./value-and-observation.js"; import * as G from "./guards.js"; import { POSIXTime } from "@marlowe.io/adapter/time"; +import * as Big from "@marlowe.io/adapter/bigint"; export { Payment, @@ -383,10 +384,6 @@ function giveMoney( ]; } -// TODO: Move to adapter -const minBigint = (a: bigint, b: bigint): bigint => (a < b ? a : b); -const maxBigint = (a: bigint, b: bigint): bigint => (a > b ? a : b); - /** * @hidden */ @@ -433,7 +430,7 @@ function reduceContractStep( }); } const balance = moneyInAccount(from_account, token, state.accounts); - const paidAmount = minBigint(amountToPay, balance); + const paidAmount = Big.min(amountToPay, balance); const newBalance = balance - paidAmount; const newAccs = updateMoneyInAccount( from_account, @@ -929,7 +926,7 @@ function fixInterval( return invalidInterval(interval.from, interval.to); if (interval.to < state.minTime) return intervalInPastError(interval.from, interval.to, state.minTime); - const newFrom = maxBigint(interval.from, state.minTime); + const newFrom = Big.max(interval.from, state.minTime); const environment = { timeInterval: { from: newFrom, to: interval.to } }; return intervalTrimmed({ environment, From 342ce75e96d647eaa94157c95e5db9db4fa499c7 Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Mon, 8 Jan 2024 17:04:04 -0300 Subject: [PATCH 16/22] Apply coderabbit suggestion Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- examples/nodejs/src/wallet-flow.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/nodejs/src/wallet-flow.ts b/examples/nodejs/src/wallet-flow.ts index 0090a6f0..b1cb82c9 100644 --- a/examples/nodejs/src/wallet-flow.ts +++ b/examples/nodejs/src/wallet-flow.ts @@ -49,4 +49,6 @@ async function main() { log("Wallet flow done 🎉"); } -main(); +main().catch(error => { + console.error("Error during main execution:", error); +}); From db0c55ed34af495233082d7807af4108922566bb Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Mon, 8 Jan 2024 17:24:54 -0300 Subject: [PATCH 17/22] Add Changelog --- ...91_add_merkleized_contract_flow_example.md | 35 +++++++++++++++++++ changelog.d/scriv.ini | 2 +- .../applicable-inputs.ts | 1 - packages/language/core/v1/src/contract.ts | 6 ++-- 4 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 changelog.d/20240108_165452_hrajchert_plt_8891_add_merkleized_contract_flow_example.md diff --git a/changelog.d/20240108_165452_hrajchert_plt_8891_add_merkleized_contract_flow_example.md b/changelog.d/20240108_165452_hrajchert_plt_8891_add_merkleized_contract_flow_example.md new file mode 100644 index 00000000..530d6517 --- /dev/null +++ b/changelog.d/20240108_165452_hrajchert_plt_8891_add_merkleized_contract_flow_example.md @@ -0,0 +1,35 @@ + +### General + +- Feat: Added debugging configuration for VSCode. Now if you are developing with VSCode you can open the folder as a workspace and the [Javascript Debug Terminal](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_javascript-debug-terminal) will have the appropiate source maps. ([PR#136](https://github.com/input-output-hk/marlowe-ts-sdk/pull/136)). +- Feat: Started an experimental getApplicableActions that should replace the current getApplicableInputs. ([PR#136](https://github.com/input-output-hk/marlowe-ts-sdk/pull/136)) + +### Examples +- Feat: Added a new interactive NodeJs example to make delayed payments with staking and merkleization. ([PR#136](https://github.com/input-output-hk/marlowe-ts-sdk/pull/136)) + + +### @marlowe.io/adapter + +- Feat: Added a bigint utilities adapter. ([PR#136](https://github.com/input-output-hk/marlowe-ts-sdk/pull/136)) +- Feat: Added iso8601ToPosixTime to the time adapter. ([PR#136](https://github.com/input-output-hk/marlowe-ts-sdk/pull/136)) + +### @marlowe.io/language-core-v1 + +- Feat: Added SingleInputTx to capture a single step transaction (either a single input or an empty tx). ([PR#136](https://github.com/input-output-hk/marlowe-ts-sdk/pull/136)). +- Feat: Added getNextTimeout to see what is the next timeout of a contract. ([PR#136](https://github.com/input-output-hk/marlowe-ts-sdk/pull/136)). +- Fix: Fix how merkleized inputs are serialized + + +### @marlowe.io/runtime-rest-client + +- [Breaking Change] Refactor: Create contract sources now uses a single parameter ContractBundle, instead of two separate bundle and main entrypoint parameters. ([PR#136](https://github.com/input-output-hk/marlowe-ts-sdk/pull/136)) +- [Breaking change] Fix: Pagination responses not always return a current header. ([PR#136](https://github.com/input-output-hk/marlowe-ts-sdk/pull/136)) + +### @marlowe.io/runtime-lifecycle + +- Feat: Added restClient to the lifecycle object for easier querying. ([PR#136](https://github.com/input-output-hk/marlowe-ts-sdk/pull/136)) +- Feat: Added getInputHistory to get a list of SingleInputTx applied to a contract. ([PR#136](https://github.com/input-output-hk/marlowe-ts-sdk/pull/136)) + +### @marlowe.io/marlowe-object + +- Feat: Added ContractBundle to represent a bundle with a main entrypoint. ([PR#136](https://github.com/input-output-hk/marlowe-ts-sdk/pull/136)) diff --git a/changelog.d/scriv.ini b/changelog.d/scriv.ini index 146c8711..38e41650 100644 --- a/changelog.d/scriv.ini +++ b/changelog.d/scriv.ini @@ -1,3 +1,3 @@ [scriv] format = md -categories = General, @marlowe.io/wallet, @marlowe.io/adapter, @marlowe.io/language-core-v1, @marlowe.io/language-examples, @marlowe.io/runtime-rest-client, @marlowe.io/runtime-core, @marlowe.io/runtime-lifecycle, @marlowe.io/language-examples +categories = General, Examples, @marlowe.io/wallet, @marlowe.io/adapter, @marlowe.io/language-core-v1, @marlowe.io/language-examples, @marlowe.io/runtime-rest-client, @marlowe.io/runtime-core, @marlowe.io/runtime-lifecycle, @marlowe.io/language-examples, @marlowe.io/marlowe-object diff --git a/examples/nodejs/src/experimental-features/applicable-inputs.ts b/examples/nodejs/src/experimental-features/applicable-inputs.ts index b188d9a1..b6a9a045 100644 --- a/examples/nodejs/src/experimental-features/applicable-inputs.ts +++ b/examples/nodejs/src/experimental-features/applicable-inputs.ts @@ -318,7 +318,6 @@ const accumulatorFromNotify = (action: CanNotify) => { notifies: action, }; }; -// TODO: Move to adapter function mergeBounds(bounds: Bound[]): Bound[] { const mergedBounds: Bound[] = []; diff --git a/packages/language/core/v1/src/contract.ts b/packages/language/core/v1/src/contract.ts index 294fb004..4feae4d5 100644 --- a/packages/language/core/v1/src/contract.ts +++ b/packages/language/core/v1/src/contract.ts @@ -13,7 +13,7 @@ import { Action, ActionGuard } from "./actions.js"; import { pipe } from "fp-ts/lib/function.js"; import getUnixTime from "date-fns/getUnixTime/index.js"; import { BuiltinByteString } from "./inputs.js"; - +import * as Big from "@marlowe.io/adapter/bigint" /** * Search [[lower-name-builders]] * @hidden @@ -331,8 +331,6 @@ export function matchContract(matcher: Partial>) { } }; } -// Copied from semantic module, maybe we want to move this to a common place? An adaptor bigint maybe? -const minBigint = (a: bigint, b: bigint): bigint => (a < b ? a : b); /** * This function calculates the next timeout of a contract after a given minTime. @@ -349,7 +347,7 @@ export function getNextTimeout(contract: Contract, minTime: Timeout): Timeout | const thenTimeout = getNextTimeout(ifContract.then, minTime); const elseTimeout = getNextTimeout(ifContract.else, minTime); return thenTimeout && elseTimeout - ? minBigint(thenTimeout, elseTimeout) + ? Big.min(thenTimeout, elseTimeout) : thenTimeout || elseTimeout; }, when: (whenContract) => { From d801eac3c621e2dcfccff871a28a4a2ae3f97c00 Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Tue, 9 Jan 2024 13:14:14 -0300 Subject: [PATCH 18/22] Make sure example folder also gets an npm install --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index b18d5394..f8406895 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest", "docs": "typedoc . --treatWarningsAsErrors --options ./typedoc.json", "serve": "ws --port 1337 --rewrite '/importmap -> https://cdn.jsdelivr.net/gh/input-output-hk/marlowe-ts-sdk@0.2.0-beta/jsdelivr-npm-importmap.js'", - "serve-dev": "ws --port 1337 --rewrite '/importmap -> /dist/local-importmap.js'" + "serve-dev": "ws --port 1337 --rewrite '/importmap -> /dist/local-importmap.js'", + "postinstall": "cd examples/nodejs && npm install" }, "workspaces": [ "packages/adapter", From 5b7506f65dca1f77f9afb3039cbae48abd81b8be Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Tue, 9 Jan 2024 13:53:05 -0300 Subject: [PATCH 19/22] fixed formatting --- .../applicable-inputs.ts | 15 ++++---- examples/nodejs/src/marlowe-object-flow.ts | 15 ++++---- examples/nodejs/src/wallet-flow.ts | 2 +- packages/adapter/package.json | 1 - packages/language/core/v1/src/contract.ts | 11 +++--- packages/language/core/v1/src/index.ts | 2 +- packages/language/core/v1/src/inputs.ts | 23 ++++++------ packages/language/core/v1/src/semantics.ts | 36 +++++++++---------- packages/runtime/client/rest/src/index.ts | 2 +- .../lifecycle/src/generic/contracts.ts | 9 +++-- tsconfig.json | 3 +- 11 files changed, 60 insertions(+), 59 deletions(-) diff --git a/examples/nodejs/src/experimental-features/applicable-inputs.ts b/examples/nodejs/src/experimental-features/applicable-inputs.ts index b6a9a045..4c4e589d 100644 --- a/examples/nodejs/src/experimental-features/applicable-inputs.ts +++ b/examples/nodejs/src/experimental-features/applicable-inputs.ts @@ -127,10 +127,9 @@ export async function getApplicableActions( let applicableActions = [] as ApplicableAction[]; const contractDetails = await restClient.getContractById(contractId); - const currentContract = - contractDetails.currentContract - ? contractDetails.currentContract - : contractDetails.initialContract; + const currentContract = contractDetails.currentContract + ? contractDetails.currentContract + : contractDetails.initialContract; const oneDayFrom = (time: Timeout) => time + 24n * 60n * 60n * 1000n; // in milliseconds const now = datetoTimeout(new Date()); const nextTimeout = getNextTimeout(currentContract, now) ?? oneDayFrom(now); @@ -322,19 +321,21 @@ const accumulatorFromNotify = (action: CanNotify) => { function mergeBounds(bounds: Bound[]): Bound[] { const mergedBounds: Bound[] = []; - const sortedBounds = [...bounds].sort((a, b) => (a.from > b.from ? 1 : a.from < b.from ? -1 : 0)); + const sortedBounds = [...bounds].sort((a, b) => + a.from > b.from ? 1 : a.from < b.from ? -1 : 0 + ); let currentBound: Bound | null = null; for (const bound of sortedBounds) { if (currentBound === null) { - currentBound = {...bound}; + currentBound = { ...bound }; } else { if (bound.from <= currentBound.to) { currentBound.to = Big.max(currentBound.to, bound.to); } else { mergedBounds.push(currentBound); - currentBound = {...bound}; + currentBound = { ...bound }; } } } diff --git a/examples/nodejs/src/marlowe-object-flow.ts b/examples/nodejs/src/marlowe-object-flow.ts index 6d3f2737..95952f52 100644 --- a/examples/nodejs/src/marlowe-object-flow.ts +++ b/examples/nodejs/src/marlowe-object-flow.ts @@ -572,9 +572,8 @@ async function createContract( const contractBundle = mkDelayPayment(schema); const tags = mkDelayPaymentTags(schema); // TODO: PLT-9089: Modify runtimeLifecycle.contracts.createContract to support bundle (calling createContractSources) - const contractSources = await lifecycle.restClient.createContractSources( - contractBundle - ); + const contractSources = + await lifecycle.restClient.createContractSources(contractBundle); const walletAddress = await lifecycle.wallet.getChangeAddress(); const unsignedTx = await lifecycle.restClient.buildCreateContractTx({ sourceId: contractSources.contractSourceId, @@ -607,9 +606,8 @@ async function validateExistingContract( contractId: ContractId ): Promise { // First we try to fetch the contract details and the required tags - const contractDetails = await lifecycle.restClient.getContractById( - contractId - ); + const contractDetails = + await lifecycle.restClient.getContractById(contractId); const scheme = extractSchemeFromTags(contractDetails.tags); @@ -629,9 +627,8 @@ async function validateExistingContract( // Or this option which doesn't require runtime to runtime communication, and just requires // the dapp to be able to recreate the same sources. const contractBundle = mkDelayPayment(scheme); - const { contractSourceId } = await lifecycle.restClient.createContractSources( - contractBundle - ); + const { contractSourceId } = + await lifecycle.restClient.createContractSources(contractBundle); const initialContract = await lifecycle.restClient.getContractSourceById({ contractSourceId, }); diff --git a/examples/nodejs/src/wallet-flow.ts b/examples/nodejs/src/wallet-flow.ts index b1cb82c9..7e3aff86 100644 --- a/examples/nodejs/src/wallet-flow.ts +++ b/examples/nodejs/src/wallet-flow.ts @@ -49,6 +49,6 @@ async function main() { log("Wallet flow done 🎉"); } -main().catch(error => { +main().catch((error) => { console.error("Error during main execution:", error); }); diff --git a/packages/adapter/package.json b/packages/adapter/package.json index f6eeba52..a9fdd747 100644 --- a/packages/adapter/package.json +++ b/packages/adapter/package.json @@ -72,7 +72,6 @@ "require": "./dist/bundled/cjs/bigint.cjs", "types": "./dist/esm/bigint.d.ts" } - }, "dependencies": { "date-fns": "2.29.3", diff --git a/packages/language/core/v1/src/contract.ts b/packages/language/core/v1/src/contract.ts index 4feae4d5..c6b8ad95 100644 --- a/packages/language/core/v1/src/contract.ts +++ b/packages/language/core/v1/src/contract.ts @@ -13,7 +13,7 @@ import { Action, ActionGuard } from "./actions.js"; import { pipe } from "fp-ts/lib/function.js"; import getUnixTime from "date-fns/getUnixTime/index.js"; import { BuiltinByteString } from "./inputs.js"; -import * as Big from "@marlowe.io/adapter/bigint" +import * as Big from "@marlowe.io/adapter/bigint"; /** * Search [[lower-name-builders]] * @hidden @@ -339,7 +339,10 @@ export function matchContract(matcher: Partial>) { * @returns The next timeout after minTime, or undefined if there is no timeout after minTime. * @category Introspection */ -export function getNextTimeout(contract: Contract, minTime: Timeout): Timeout | undefined { +export function getNextTimeout( + contract: Contract, + minTime: Timeout +): Timeout | undefined { return matchContract({ close: () => undefined, pay: (pay) => getNextTimeout(pay.then, minTime), @@ -358,6 +361,6 @@ export function getNextTimeout(contract: Contract, minTime: Timeout): Timeout | } }, let: (letContract) => getNextTimeout(letContract.then, minTime), - assert: (assertContract) => getNextTimeout(assertContract.then, minTime) - })(contract) + assert: (assertContract) => getNextTimeout(assertContract.then, minTime), + })(contract); } diff --git a/packages/language/core/v1/src/index.ts b/packages/language/core/v1/src/index.ts index d2eec463..75db672e 100644 --- a/packages/language/core/v1/src/index.ts +++ b/packages/language/core/v1/src/index.ts @@ -63,7 +63,7 @@ export { datetoTimeout, timeoutToDate, Timeout, - getNextTimeout + getNextTimeout, } from "./contract.js"; export { Environment, mkEnvironment, TimeInterval } from "./environment.js"; diff --git a/packages/language/core/v1/src/inputs.ts b/packages/language/core/v1/src/inputs.ts index 851aaacc..57ac9a74 100644 --- a/packages/language/core/v1/src/inputs.ts +++ b/packages/language/core/v1/src/inputs.ts @@ -121,17 +121,17 @@ export interface MerkleizedHashAndContinuation { merkleized_continuation: Contract; } -export const MerkleizedHashAndContinuationGuard: t.Type = t.type({ - continuation_hash: BuiltinByteStringGuard, - merkleized_continuation: ContractGuard, -}); +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 const MerkleizedDepositGuard: t.Type = t.intersection( + [IDepositGuard, MerkleizedHashAndContinuationGuard] +); export type MerkleizedChoice = IChoice & MerkleizedHashAndContinuation; @@ -149,12 +149,15 @@ export const MerkleizedNotifyGuard = MerkleizedHashAndContinuationGuard; * TODO: Revisit * @category Input */ -export type MerkleizedInput = MerkleizedDeposit | MerkleizedChoice | MerkleizedNotify; +export type MerkleizedInput = + | MerkleizedDeposit + | MerkleizedChoice + | MerkleizedNotify; /** * TODO: Revisit * @category Input */ -export const MerkleizedInputGuard =t.union([ +export const MerkleizedInputGuard = t.union([ MerkleizedDepositGuard, MerkleizedChoiceGuard, MerkleizedNotifyGuard, diff --git a/packages/language/core/v1/src/semantics.ts b/packages/language/core/v1/src/semantics.ts index b9bf9ad4..51f3f4d5 100644 --- a/packages/language/core/v1/src/semantics.ts +++ b/packages/language/core/v1/src/semantics.ts @@ -111,7 +111,7 @@ export { TransactionSuccess, TransactionOutput, } from "./transaction.js"; -export {inBounds}; +export { inBounds }; /** * The function moneyInAccount returns the number of tokens a particular AccountId has in their account. * @hidden @@ -532,19 +532,18 @@ function reduceContractStep( /** * @hidden */ -export type ContractQuiescentReduceResult = - { - type: "ContractQuiescent"; - reduced: boolean; - state: MarloweState; - warnings: ReduceWarning[]; - payments: Payment[]; - continuation: Contract; - } +export type ContractQuiescentReduceResult = { + type: "ContractQuiescent"; + reduced: boolean; + state: MarloweState; + warnings: ReduceWarning[]; + payments: Payment[]; + continuation: Contract; +}; /** * @hidden */ -type ReduceResult = ContractQuiescentReduceResult| AmbiguousTimeIntervalError; +type ReduceResult = ContractQuiescentReduceResult | AmbiguousTimeIntervalError; /** * @hidden @@ -717,20 +716,19 @@ const hashMismatchError = "TEHashMismatch" as const; type ApplyResult = AppliedResult | ApplyNoMatchError | HashMismatchError; - -function inputToInputContent (input: Input): InputContent { +function inputToInputContent(input: Input): InputContent { if (input === "input_notify") { return "input_notify"; } if ("that_deposits" in input) { - input - return input as IDeposit + input; + return input as IDeposit; } if ("input_that_chooses_num" in input) { - input + input; return input as IChoice; } - return "input_notify" + return "input_notify"; } /** * @hidden @@ -787,7 +785,9 @@ type TransactionWarning = | Shadowing | AssertionFailed; -export function convertReduceWarning(warnings: ReduceWarning[]): TransactionWarning[] { +export function convertReduceWarning( + warnings: ReduceWarning[] +): TransactionWarning[] { return warnings.filter((w) => w !== "NoWarning") as TransactionWarning[]; } diff --git a/packages/runtime/client/rest/src/index.ts b/packages/runtime/client/rest/src/index.ts index effc67d7..7f52b3fd 100644 --- a/packages/runtime/client/rest/src/index.ts +++ b/packages/runtime/client/rest/src/index.ts @@ -313,7 +313,7 @@ export function mkRestClient(baseURL: string): RestClient { ) ); }, - createContractSources({main, bundle}) { + createContractSources({ main, bundle }) { return Sources.createContractSources(axiosInstance)(main, bundle); }, getContractSourceById(request) { diff --git a/packages/runtime/lifecycle/src/generic/contracts.ts b/packages/runtime/lifecycle/src/generic/contracts.ts index e55fe25d..74053fb8 100644 --- a/packages/runtime/lifecycle/src/generic/contracts.ts +++ b/packages/runtime/lifecycle/src/generic/contracts.ts @@ -54,9 +54,8 @@ export function mkContractLifecycle( const getInputHistory = ({ restClient }: ContractsDI) => async (contractId: ContractId): Promise => { - const transactionHeaders = await restClient.getTransactionsForContract( - contractId - ); + const transactionHeaders = + await restClient.getTransactionsForContract(contractId); const transactions = await Promise.all( transactionHeaders.transactions.map((txHeader) => restClient.getContractTransactionById( @@ -70,8 +69,8 @@ const getInputHistory = b: Option ) => { if (a._tag === "None" || b._tag === "None") { - // TODO: to avoid this error we should provide a higer level function that gets the transactions as the different - // status and with the appropiate values for each state. + // TODO: to avoid this error we should provide a higer level function that gets the transactions as the different + // status and with the appropiate values for each state. throw new Error("A confirmed transaction should have a valid block"); } else { if (a.value.blockNo < b.value.blockNo) { diff --git a/tsconfig.json b/tsconfig.json index eff7641f..3d8e8aaf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,6 @@ { "path": "./packages/runtime/lifecycle/src" }, { "path": "./packages/token-metadata-client/src" }, { "path": "./packages/marlowe-object/src" }, - { "path": "./examples/nodejs/src" }, - + { "path": "./examples/nodejs/src" } ] } From fd9f7d947b6c9aad05851dcb9edb8040be872732 Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Tue, 9 Jan 2024 14:50:15 -0300 Subject: [PATCH 20/22] Added PR suggestions --- .../applicable-inputs.ts | 40 +++++++++---------- examples/nodejs/src/marlowe-object-flow.ts | 28 ++++++------- 2 files changed, 31 insertions(+), 37 deletions(-) diff --git a/examples/nodejs/src/experimental-features/applicable-inputs.ts b/examples/nodejs/src/experimental-features/applicable-inputs.ts index 4c4e589d..415be0dd 100644 --- a/examples/nodejs/src/experimental-features/applicable-inputs.ts +++ b/examples/nodejs/src/experimental-features/applicable-inputs.ts @@ -124,9 +124,7 @@ export async function getApplicableActions( contractId: ContractId, environment?: Environment ): Promise { - let applicableActions = [] as ApplicableAction[]; const contractDetails = await restClient.getContractById(contractId); - const currentContract = contractDetails.currentContract ? contractDetails.currentContract : contractDetails.initialContract; @@ -134,7 +132,6 @@ export async function getApplicableActions( const now = datetoTimeout(new Date()); const nextTimeout = getNextTimeout(currentContract, now) ?? oneDayFrom(now); const timeInterval = { from: now, to: nextTimeout - 1n }; - const env = environment ?? { timeInterval }; if (typeof contractDetails.state === "undefined") { // TODO: Check, I believe this happens when a contract is in a closed state, but it would be nice @@ -148,22 +145,24 @@ export async function getApplicableActions( ); if (initialReduce == "TEAmbiguousTimeIntervalError") throw new Error("AmbiguousTimeIntervalError"); - if (initialReduce.reduced) { - applicableActions.push({ - type: "Advance", - policyId: contractDetails.roleTokenMintingPolicyId, - applyAction() { - return { - inputs: [], - environment: env, - reducedState: initialReduce.state, - reducedContract: initialReduce.continuation, - warnings: convertReduceWarning(initialReduce.warnings), - payments: initialReduce.payments, - }; - }, - }); - } + const applicableActions: ApplicableAction[] = initialReduce.reduced + ? [ + { + type: "Advance", + policyId: contractDetails.roleTokenMintingPolicyId, + applyAction() { + return { + inputs: [], + environment: env, + reducedState: initialReduce.state, + reducedContract: initialReduce.continuation, + warnings: convertReduceWarning(initialReduce.warnings), + payments: initialReduce.payments, + }; + }, + }, + ] + : []; const cont = initialReduce.continuation; if (cont === "close") return applicableActions; if ("when" in cont) { @@ -181,7 +180,7 @@ export async function getApplicableActions( ) ) ); - applicableActions = applicableActions.concat( + return applicableActions.concat( toApplicableActions( applicableActionsFromCases.reduce( mergeApplicableActionAccumulator.concat, @@ -190,7 +189,6 @@ export async function getApplicableActions( ) ); } - return applicableActions; } diff --git a/examples/nodejs/src/marlowe-object-flow.ts b/examples/nodejs/src/marlowe-object-flow.ts index 95952f52..26987068 100644 --- a/examples/nodejs/src/marlowe-object-flow.ts +++ b/examples/nodejs/src/marlowe-object-flow.ts @@ -1,8 +1,13 @@ /** - * This example shows how to work with the marlowe-object package, which is needed when we - * want to create large contracts through the use of Merkleization. + * This is an interactive Node.js script that uses the inquirer.js to create and interact + * with a Delayed Payment contract. * - * The script is a command line tool that makes a delay payment to a given address. + * This example features: + * - The use of inquirer.js to create an interactive command line tool + * - The use of the marlowe-object package to create a contract bundle + * - How to stake the assets of a contract to a given stake address + * - How to validate that a Merkleized contract is an instance of a given contract + * - How to share contract sources between different runtimes */ import { mkLucidWallet, WalletAPI } from "@marlowe.io/wallet"; import { mkRuntimeLifecycle } from "@marlowe.io/runtime-lifecycle"; @@ -73,20 +78,11 @@ function parseCli() { */ async function waitIndicator(wallet: WalletAPI, txId: TxId) { process.stdout.write("Waiting for the transaction to be confirmed..."); - let done = false; - function writeDot(): Promise { + const intervalId = setInterval(() => { process.stdout.write("."); - return new Promise((resolve) => setTimeout(resolve, 1000)).then(() => { - if (!done) { - return writeDot(); - } - }); - } - - await Promise.all([ - wallet.waitConfirmation(txId).then(() => (done = true)), - writeDot(), - ]); + }, 1000); + await wallet.waitConfirmation(txId); + clearInterval(intervalId); process.stdout.write("\n"); } From c2480fafaec6ab72ee48889f3a72e74adadbd579 Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Tue, 9 Jan 2024 15:18:45 -0300 Subject: [PATCH 21/22] Fix doc warnings --- .../applicable-inputs.ts | 4 +--- packages/language/core/v1/src/index.ts | 4 ++++ packages/language/core/v1/src/semantics.ts | 19 +++++++------------ 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/examples/nodejs/src/experimental-features/applicable-inputs.ts b/examples/nodejs/src/experimental-features/applicable-inputs.ts index 415be0dd..32084d14 100644 --- a/examples/nodejs/src/experimental-features/applicable-inputs.ts +++ b/examples/nodejs/src/experimental-features/applicable-inputs.ts @@ -19,9 +19,6 @@ import { Case, Action, Notify, - IDeposit, - IChoice, - INotify, InputContent, RoleName, Token, @@ -43,6 +40,7 @@ import { WalletAPI } from "@marlowe.io/wallet"; import * as Big from "@marlowe.io/adapter/bigint"; import { Monoid } from "fp-ts/lib/Monoid.js"; import * as R from "fp-ts/lib/Record.js"; + type ActionApplicant = Party | "anybody"; export interface AppliedActionResult { diff --git a/packages/language/core/v1/src/index.ts b/packages/language/core/v1/src/index.ts index 75db672e..52d90d5d 100644 --- a/packages/language/core/v1/src/index.ts +++ b/packages/language/core/v1/src/index.ts @@ -77,6 +77,10 @@ export { InputContent, NormalInput, MerkleizedInput, + MerkleizedDeposit, + MerkleizedChoice, + MerkleizedHashAndContinuation, + MerkleizedNotify, } from "./inputs.js"; export { role, Party, Address, Role, RoleName } from "./participants.js"; diff --git a/packages/language/core/v1/src/semantics.ts b/packages/language/core/v1/src/semantics.ts index 51f3f4d5..c79f6fc1 100644 --- a/packages/language/core/v1/src/semantics.ts +++ b/packages/language/core/v1/src/semantics.ts @@ -78,6 +78,7 @@ import { Transaction, TransactionError, TransactionOutput, + TransactionWarning, } from "./transaction.js"; import { matchObservation, @@ -292,9 +293,10 @@ const shadowing = (warn: Shadowing): Shadowing => warn; const assertionFailed = "assertion_failed" as const; /** - * @hidden + * TODO: Comment + * @category Transaction Warning */ -type NoWarning = "NoWarning"; +export type NoWarning = "NoWarning"; /** * @hidden @@ -302,9 +304,10 @@ type NoWarning = "NoWarning"; const noWarning = "NoWarning" as const; /** - * @hidden + * TODO: Comment + * @category Transaction Warning */ -type ReduceWarning = +export type ReduceWarning = | NoWarning | NonPositivePay | PartialPay @@ -777,14 +780,6 @@ function applyInput( ); } -// TODO: I think we have to move this to transaction.ts and make sure JSON serialization aligns. -type TransactionWarning = - | NonPositiveDeposit - | NonPositivePay - | PartialPay - | Shadowing - | AssertionFailed; - export function convertReduceWarning( warnings: ReduceWarning[] ): TransactionWarning[] { From cc1b883889f862ec025d4c5c1ee7294186946146 Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Wed, 10 Jan 2024 12:02:08 -0300 Subject: [PATCH 22/22] Added more PR feedback --- examples/nodejs/Readme.md | 2 +- .../applicable-inputs.ts | 34 +++++++++---------- examples/nodejs/src/marlowe-object-flow.ts | 6 +++- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/examples/nodejs/Readme.md b/examples/nodejs/Readme.md index adbef0dc..e20a04e3 100644 --- a/examples/nodejs/Readme.md +++ b/examples/nodejs/Readme.md @@ -15,7 +15,7 @@ Then you need to create a `.config.json` file with the following content // https://blockfrost.io/ "blockfrostProjectId": "YOUR_PROJECT_ID", "blockfrostUrl": "https://cardano-preprod.blockfrost.io/api/v0", - "blockfrostNetwork": "Preprod", + "network": "Preprod", // You can create this seed phrase from any wallet. Do not reuse a real wallet phrase // for a test example. "seedPhrase": "alpha beta delta...", diff --git a/examples/nodejs/src/experimental-features/applicable-inputs.ts b/examples/nodejs/src/experimental-features/applicable-inputs.ts index 32084d14..a6a1dd18 100644 --- a/examples/nodejs/src/experimental-features/applicable-inputs.ts +++ b/examples/nodejs/src/experimental-features/applicable-inputs.ts @@ -95,7 +95,7 @@ interface CanChoose { /** * This Applicable action is intended to be used when the contract is not in a quiescent state. - * This means that the contract is either timeouted, or it was just created and it doesn't starts with a `When` + * This means that the contract is either timed out, or it was just created and it doesn't starts with a `When` */ interface CanAdvance { type: "Advance"; @@ -165,7 +165,7 @@ export async function getApplicableActions( if (cont === "close") return applicableActions; if ("when" in cont) { const applicableActionsFromCases = await Promise.all( - cont.when.map((cse) => + cont.when.map((aCase) => getApplicableActionFromCase( restClient, env, @@ -173,7 +173,7 @@ export async function getApplicableActions( initialReduce.state, initialReduce.payments, convertReduceWarning(initialReduce.warnings), - cse, + aCase, contractDetails.roleTokenMintingPolicyId ) ) @@ -388,22 +388,22 @@ async function getApplicableActionFromCase( state: MarloweState, previousPayments: Payment[], previousWarnings: TransactionWarning[], - cse: Case, + aCase: Case, policyId: PolicyId ): Promise { - let cseContinuation: Contract; - if ("merkleized_then" in cse) { - cseContinuation = await restClient.getContractSourceById({ - contractSourceId: cse.merkleized_then, + let aCaseContinuation: Contract; + if ("merkleized_then" in aCase) { + aCaseContinuation = await restClient.getContractSourceById({ + contractSourceId: aCase.merkleized_then, }); } else { - cseContinuation = cse.then; + aCaseContinuation = aCase.then; } function decorateInput(content: InputContent): Input { - if ("merkleized_then" in cse) { + if ("merkleized_then" in aCase) { const merkleizedHashAndContinuation = { - continuation_hash: cse.merkleized_then, - merkleized_continuation: cseContinuation, + continuation_hash: aCase.merkleized_then, + merkleized_continuation: aCaseContinuation, }; // MerkleizedNotify are serialized as the plain merkle object if (content === "input_notify") { @@ -420,8 +420,8 @@ async function getApplicableActionFromCase( } } - if (isDepositAction(cse.case)) { - const deposit = cse.case; + if (isDepositAction(aCase.case)) { + const deposit = aCase.case; return accumulatorFromDeposit(env, state, { type: "Deposit", deposit, @@ -450,8 +450,8 @@ async function getApplicableActionFromCase( }; }, }); - } else if (isChoice(cse.case)) { - const choice = cse.case; + } else if (isChoice(aCase.case)) { + const choice = aCase.case; return accumulatorFromChoice({ type: "Choice", @@ -482,7 +482,7 @@ async function getApplicableActionFromCase( }, }); } else { - const notify = cse.case; + const notify = aCase.case; if (!evalObservation(env, state, notify.notify_if)) { return mergeApplicableActionAccumulator.empty; } diff --git a/examples/nodejs/src/marlowe-object-flow.ts b/examples/nodejs/src/marlowe-object-flow.ts index 26987068..720bd30e 100644 --- a/examples/nodejs/src/marlowe-object-flow.ts +++ b/examples/nodejs/src/marlowe-object-flow.ts @@ -655,5 +655,9 @@ async function main() { runtimeURL, wallet, }); - await mainLoop(lifecycle, rewardAddress); + try { + await mainLoop(lifecycle, rewardAddress); + } catch (e) { + console.log(`Error : ${JSON.stringify(e, null, 4)}`); + } }