diff --git a/examples/nodejs/src/marlowe-object-flow.ts b/examples/nodejs/src/marlowe-object-flow.ts index 754bf2f3..e9b57387 100644 --- a/examples/nodejs/src/marlowe-object-flow.ts +++ b/examples/nodejs/src/marlowe-object-flow.ts @@ -15,8 +15,10 @@ import { Lucid, Blockfrost, C } from "lucid-cardano"; import { readConfig } from "./config.js"; import { datetoTimeout, When } from "@marlowe.io/language-core-v1"; import { + addressBech32, contractId, ContractId, + Metadata, stakeAddressBech32, StakeAddressBech32, Tags, @@ -38,6 +40,7 @@ import { mkSourceMap, SourceMap } from "./experimental-features/source-map.js"; import { POSIXTime, posixTimeToIso8601 } from "@marlowe.io/adapter/time"; import { SingleInputTx } from "@marlowe.io/language-core-v1/semantics"; import * as ObjG from "@marlowe.io/marlowe-object/guards"; +import { BlueprintOf, mkBlueprint } from "@marlowe.io/blueprint"; // When this script is called, start with main. main(); @@ -139,10 +142,12 @@ async function createContractMenu( lifecycle: RuntimeLifecycle, rewardAddress?: StakeAddressBech32 ) { - const payee = await input({ - message: "Enter the payee address", - validate: bech32Validator, - }); + const payee = addressBech32( + await input({ + message: "Enter the payee address", + validate: bech32Validator, + }) + ); const amountStr = await input({ message: "Enter the payment amount in lovelaces", validate: positiveBigIntValidator, @@ -176,17 +181,18 @@ async function createContractMenu( } const scheme = { - payFrom: { address: walletAddress }, - payTo: { address: payee }, + payer: walletAddress, + payee, amount, depositDeadline, releaseDeadline, }; - const tags = mkDelayPaymentTags(scheme); + const metadata = delayPaymentBlueprint.encode(scheme); const sourceMap = await mkSourceMap(lifecycle, mkDelayPayment(scheme)); const [contractId, txId] = await sourceMap.createContract({ stakeAddress: rewardAddress, - tags, + tags: { DELAY_PAYMENT_VERSION: "2" }, + metadata, }); console.log(`Contract created with id ${contractId}`); @@ -209,7 +215,7 @@ async function loadContractMenu(lifecycle: RuntimeLifecycle) { const cid = contractId(cidStr); // Then we make sure that contract id is an instance of our delayed payment contract const validationResult = await validateExistingContract(lifecycle, cid); - if (validationResult === "InvalidTags") { + if (validationResult === "InvalidBlueprint") { console.log("Invalid contract, it does not have the expected tags"); return; } @@ -222,8 +228,8 @@ async function loadContractMenu(lifecycle: RuntimeLifecycle) { // If it is, we print the contract details and go to the contract menu console.log("Contract details:"); - console.log(` * Pay from: ${validationResult.scheme.payFrom.address}`); - console.log(` * Pay to: ${validationResult.scheme.payTo.address}`); + console.log(` * Pay from: ${validationResult.scheme.payer}`); + console.log(` * Pay to: ${validationResult.scheme.payee}`); console.log(` * Amount: ${validationResult.scheme.amount} lovelaces`); console.log( ` * Deposit deadline: ${validationResult.scheme.depositDeadline}` @@ -374,32 +380,46 @@ async function mainLoop( // #endregion // #region Delay Payment Contract + +const delayPaymentBlueprint = mkBlueprint({ + name: "Delayed payment", + description: + "In a delay payment, a `payer` transfer an `amount` of ADA to the `payee` which can be redeemed after a `releaseDeadline`. While the payment is held by the contract, it can be staked to the payer, to generate pasive income while the payee has the guarantees that the money will be released.", + params: [ + { + name: "payer", + description: "Who is making the payment", + type: "address", + }, + { + name: "payee", + description: "Who is receiving the payment", + type: "address", + }, + { + name: "amount", + description: "The amount of lovelaces to be paid", + type: "value", + }, + { + name: "depositDeadline", + description: + "The deadline for the payment to be made. If the payment is not made by this date, the contract can be closed", + type: "date", + }, + { + name: "releaseDeadline", + description: + "A date after the payment can be released to the receiver. NOTE: An empty transaction must be done to close the contract", + type: "date", + }, + ] as const, +}); + /** * 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; -} +type DelayPaymentScheme = BlueprintOf; type DelayPaymentAnnotations = | "initialDeposit" @@ -436,10 +456,10 @@ function mkDelayPayment( when: [ { case: { - party: scheme.payFrom, - deposits: scheme.amount, + party: { address: scheme.payer }, + deposits: BigInt(scheme.amount), of_token: lovelace, - into_account: scheme.payTo, + into_account: { address: scheme.payee }, }, then: { ref: "release-funds", @@ -503,9 +523,7 @@ type Closed = { 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.payer} to deposit ${scheme.amount}`); break; case "PaymentDeposited": console.log( @@ -562,52 +580,8 @@ function getState( // #endregion -const mkDelayPaymentTags = (schema: DelayPaymentScheme) => { - 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; -}; - -const extractSchemeFromTags = ( - tags: unknown -): DelayPaymentScheme | undefined => { - 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)) { - return; - } - - 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"]), - }; -}; - type ValidationResults = - | "InvalidTags" + | "InvalidBlueprint" | "InvalidContract" | { scheme: DelayPaymentScheme; @@ -629,10 +603,10 @@ async function validateExistingContract( contractId, }); - const scheme = extractSchemeFromTags(contractDetails.tags); + const scheme = delayPaymentBlueprint.decode(contractDetails.metadata); if (!scheme) { - return "InvalidTags"; + return "InvalidBlueprint"; } // If the contract seems to be an instance of the contract we want (meanin, we were able diff --git a/jsdelivr-npm-importmap.js b/jsdelivr-npm-importmap.js index 2b6ab86b..dd19ee6e 100644 --- a/jsdelivr-npm-importmap.js +++ b/jsdelivr-npm-importmap.js @@ -22,6 +22,8 @@ const importMap = { "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta/dist/bundled/esm/lucid.js", "@marlowe.io/adapter/time": "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta/dist/bundled/esm/time.js", + "@marlowe.io/blueprint": + "https://cdn.jsdelivr.net/npm/@marlowe.io/blueprint@0.3.0-beta/dist/bundled/esm/blueprint.js", "@marlowe.io/language-core-v1": "https://cdn.jsdelivr.net/npm/@marlowe.io/language-core-v1@0.3.0-beta/dist/bundled/esm/language-core-v1.js", "@marlowe.io/language-core-v1/guards": diff --git a/package-lock.json b/package-lock.json index 9250feb8..615b35fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "license": "Apache-2.0", "workspaces": [ "packages/adapter", + "packages/blueprint", "packages/language/core/v1", "packages/language/examples", "packages/language/specification-client", @@ -1404,6 +1405,10 @@ "resolved": "packages/adapter", "link": true }, + "node_modules/@marlowe.io/blueprint": { + "resolved": "packages/blueprint", + "link": true + }, "node_modules/@marlowe.io/language-core-v1": { "resolved": "packages/language/core/v1", "link": true @@ -9833,6 +9838,14 @@ "newtype-ts": "0.3.5" } }, + "packages/blueprint": { + "version": "0.3.0-beta", + "license": "Apache-2.0", + "dependencies": { + "@marlowe.io/language-core-v1": "0.3.0-beta", + "@marlowe.io/runtime-core": "0.3.0-beta" + } + }, "packages/language/core/v1": { "name": "@marlowe.io/language-core-v1", "version": "0.3.0-beta", @@ -9854,6 +9867,7 @@ "license": "Apache-2.0", "dependencies": { "@marlowe.io/language-core-v1": "0.3.0-beta", + "@marlowe.io/marlowe-object": "0.3.0-beta", "date-fns": "2.29.3", "fp-ts": "^2.16.1" } diff --git a/package.json b/package.json index a9ed2b08..d9ea4657 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "workspaces": [ "packages/adapter", + "packages/blueprint", "packages/language/core/v1", "packages/language/examples", "packages/language/specification-client", diff --git a/packages/blueprint/Readme.md b/packages/blueprint/Readme.md new file mode 100644 index 00000000..c1446d30 --- /dev/null +++ b/packages/blueprint/Readme.md @@ -0,0 +1,3 @@ +# Marlowe Blueprint + +TODO. diff --git a/packages/blueprint/src/index.ts b/packages/blueprint/src/index.ts index efcaa2cb..9e61d15a 100644 --- a/packages/blueprint/src/index.ts +++ b/packages/blueprint/src/index.ts @@ -1,50 +1 @@ -interface ContractParameter { - name: string; - description?: string; -} - -interface AddressParameter extends ContractParameter { - type: "address"; -} - -interface DateParameter extends ContractParameter { - type: "date"; -} - -interface ValueParameter extends ContractParameter { - type: "value"; -} - -type BlueprintParameter = AddressParameter | DateParameter | ValueParameter; - -type Blueprint = BlueprintParameter[]; - -const delayPaymentBlueprint: Blueprint = [ - { - name: "payFrom", - description: "Who is making the delayed payment", - type: "address", - }, - { - name: "payTo", - description: "Who is receiving the payment", - type: "address", - }, - { - name: "amount", - description: "The amount of lovelaces to be paid", - type: "value", - }, - { - name: "depositDeadline", - description: - "The deadline for the payment to be made. If the payment is not made by this date, the contract can be closed", - type: "date", - }, - { - name: "releaseDeadline", - description: - "A date after the payment can be released to the receiver. NOTE: An empty transaction must be done to close the contract", - type: "date", - }, -]; +export { Blueprint, mkBlueprint, MkBlueprint, BlueprintOf } from "./typed.js"; diff --git a/packages/blueprint/src/typed.ts b/packages/blueprint/src/typed.ts index d59717cf..3905f0e9 100644 --- a/packages/blueprint/src/typed.ts +++ b/packages/blueprint/src/typed.ts @@ -4,7 +4,6 @@ import { AddressBech32, AddressBech32Guard, Metadata, - MetadatumGuard, } from "@marlowe.io/runtime-core"; import { BigIntOrNumber, @@ -93,7 +92,7 @@ function blueprintParamsObjectGuard[]>( ); } -class Blueprint { +export class Blueprint { private blueprintCodec: t.Type, unknown>; name: string; description?: string; @@ -198,9 +197,13 @@ class Blueprint { throw new Error("Invalid value"); } } - - encode(value: T): t.OutputOf { - return this.blueprintCodec.encode(value); + // NOTE: The output of the encode is the output of the Metadata codec, + // which is compatible with its inputs, but it doesn't have the + // branded types. Here we cast to the Metadata type to allow easier + // usability with the rest of the runtime, that expects the Branded + // types. + encode(value: T): Metadata { + return this.blueprintCodec.encode(value) as Metadata; } } diff --git a/packages/blueprint/test/blueprint-delayed-payment.spec.ts b/packages/blueprint/test/blueprint-delayed-payment.spec.ts index 517cf8ac..c3dcb327 100644 --- a/packages/blueprint/test/blueprint-delayed-payment.spec.ts +++ b/packages/blueprint/test/blueprint-delayed-payment.spec.ts @@ -1,5 +1,5 @@ import { AddressBech32, addressBech32 } from "@marlowe.io/runtime-core"; -import { BlueprintOf, mkBlueprint } from "../src/typed.js"; +import { BlueprintOf, mkBlueprint } from "@marlowe.io/blueprint"; describe("Delayed payment Blueprint", () => { const delayPaymentBlueprint = mkBlueprint({ name: "Delayed payment", diff --git a/packages/blueprint/typedoc.json b/packages/blueprint/typedoc.json new file mode 100644 index 00000000..be5116be --- /dev/null +++ b/packages/blueprint/typedoc.json @@ -0,0 +1,8 @@ +{ + "entryPointStrategy": "expand", + "entryPoints": ["./src/index.ts"], + "tsconfig": "./src/tsconfig.json", + "categorizeByGroup": false, + "defaultCategory": "General", + "categoryOrder": ["*"] +}