Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental feature: Add @marlowe.io/marlowe-template package #184

Merged
merged 23 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
86385ee
Fix metadata typing
hrajchert Feb 8, 2024
c70ef58
wip initial blueprint work
hrajchert Feb 8, 2024
52c2067
wip: initial Blueprint class
hrajchert Feb 12, 2024
b959041
Added basic unit testing to blueprints
hrajchert Feb 12, 2024
f021508
Add first encode to Blueprint
hrajchert Feb 12, 2024
24d8406
First version of blueprint decode
hrajchert Feb 12, 2024
f543af1
Improved blueprint typing, added name, description and tests
hrajchert Feb 12, 2024
c09a83e
Added AddressBech32 guard in Blueprint
hrajchert Feb 13, 2024
d22ed40
Modify marlowe-object-flow to use Blueprint
hrajchert Feb 13, 2024
91fdb6b
Remove old experimenta-feature/metadata
hrajchert Feb 13, 2024
e49f93c
Fix build
hrajchert Feb 13, 2024
e4bc120
wip small refactor and documentation
hrajchert Feb 13, 2024
5618a40
Small refactor and documentation regarding blueprints
hrajchert Feb 14, 2024
dcb4cf8
Add TokenParam to Blueprint
hrajchert Feb 14, 2024
a49629e
Add scriv entry
hrajchert Feb 14, 2024
c7bf939
Fix missing export
hrajchert Feb 14, 2024
783f96d
Added preservedBrand io-ts helper
hrajchert Feb 15, 2024
712adfc
Renamed package blueprint to marlowe-template
hrajchert Feb 20, 2024
bae26a5
Renamed TemplateParametersOf and mkMarloweTemplate
hrajchert Feb 20, 2024
7725df3
Small codec refactor
hrajchert Feb 20, 2024
32dfa41
Renamed some files from blueprint to template
hrajchert Feb 20, 2024
ca22762
Removed Blueprint all around
hrajchert Feb 20, 2024
ee4bd09
Renamed toMetadata and fromMetadata
hrajchert Feb 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"outFiles": [
"${workspaceFolder}/examples/nodejs/dist/**/*.js",
"${workspaceFolder}/packages/adapter/dist/esm/**/*.js",
"${workspaceFolder}/packages/blueprint/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",
Expand All @@ -13,6 +14,7 @@
"${workspaceFolder}/packages/runtime/core/dist/esm/**/*.js",
"${workspaceFolder}/packages/runtime/lifecycle/dist/esm/**/*.js",
"${workspaceFolder}/packages/marlowe-object/dist/esm/**/*.js",
"${workspaceFolder}/packages/blueprint/test-dist/**/*.js",
"${workspaceFolder}/packages/marlowe-object/test-dist/**/*.js",
"${workspaceFolder}/packages/runtime/lifecycle/test-dist/**/*.js",
"${workspaceFolder}/packages/runtime/client/rest/test-dist/**/*.js",
Expand Down
12 changes: 12 additions & 0 deletions changelog.d/20240214_142506_hrajchert_blueprints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@


### General

- Feat: Created a new experimental package `@marlowe.io/blueprints` that helps to share the parameters used in the creation of a Marlowe contract. ([PR-184](https://github.com/input-output-hk/marlowe-ts-sdk/pull/184))


### @marlowe.io/runtime-core

- Feat: Added AddressBech32 validation using the lucid library ([PR-184](https://github.com/input-output-hk/marlowe-ts-sdk/pull/184))
- Fix: Added proper type guards to Metadata ([PR-184](https://github.com/input-output-hk/marlowe-ts-sdk/pull/184))

8 changes: 0 additions & 8 deletions examples/nodejs/src/experimental-features/metadata.ts

This file was deleted.

149 changes: 59 additions & 90 deletions examples/nodejs/src/marlowe-object-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@ 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,
stakeAddressBech32,
StakeAddressBech32,
Tags,
TxId,
} from "@marlowe.io/runtime-core";
import { Address } from "@marlowe.io/language-core-v1";
import { ContractBundleMap, lovelace, close } from "@marlowe.io/marlowe-object";
import { input, select } from "@inquirer/prompts";
import { RuntimeLifecycle } from "@marlowe.io/runtime-lifecycle/api";
Expand All @@ -32,12 +31,12 @@ import {
mkApplicableActionsFilter,
} from "./experimental-features/applicable-inputs.js";
import arg from "arg";
import { splitAddress } from "./experimental-features/metadata.js";
import * as t from "io-ts/lib/index.js";
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();
Expand Down Expand Up @@ -139,10 +138,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,
Expand Down Expand Up @@ -176,17 +177,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}`);
Expand All @@ -209,7 +211,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;
}
Expand All @@ -222,8 +224,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}`
Expand Down Expand Up @@ -374,32 +376,45 @@ 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<typeof delayPaymentBlueprint>;

type DelayPaymentAnnotations =
| "initialDeposit"
Expand Down Expand Up @@ -436,10 +451,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",
Expand Down Expand Up @@ -503,9 +518,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(
Expand Down Expand Up @@ -562,52 +575,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"]),
};
};
Comment on lines -565 to -607
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these functions that manually encoded and decoded the parameters as tags are now provided by the toMetadata and fromMetadata functions of the template parameters.


type ValidationResults =
| "InvalidTags"
| "InvalidBlueprint"
| "InvalidContract"
| {
scheme: DelayPaymentScheme;
Expand All @@ -629,10 +598,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
Expand Down
1 change: 1 addition & 0 deletions jest.unit.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module.exports = {
testEnvironment: "node",
projects: [
"<rootDir>/packages/language/core/v1/test/jest.unit.config.mjs",
"<rootDir>/packages/blueprint/test/jest.unit.config.mjs",
"<rootDir>/packages/language/examples/test/jest.unit.config.mjs",
"<rootDir>/packages/marlowe-object/test/jest.unit.config.mjs",
"<rootDir>/packages/wallet/test/jest.unit.config.mjs",
Expand Down
2 changes: 2 additions & 0 deletions jsdelivr-npm-importmap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
},
"workspaces": [
"packages/adapter",
"packages/blueprint",
"packages/language/core/v1",
"packages/language/examples",
"packages/language/specification-client",
Expand Down
24 changes: 24 additions & 0 deletions packages/adapter/src/bigint.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
/**
* Utility functions for bigint.
*/
import * as t from "io-ts/lib/index.js";

export const min = (a: bigint, b: bigint): bigint => (a < b ? a : b);
export const max = (a: bigint, b: bigint): bigint => (a > b ? a : b);

// The following type and guard are used to represent a number that can be either a bigint or a number.
// The guard always encode the number as a bigint (when outputting a value).
// NOTE: I'm not sure if is better to have this or to have a guard that accepts both bigint and number
// as input but has bigint as `A`ctual type and `O`utput type.
// A good reason to have BigIntOrNumber as Actual type is that it is easier to construct values
// (as you can omit the final `n`), but a drawback is that in usage you might need to cast
// to `Number(actual)` or `BigInt(actual)` depending on context.
export type BigIntOrNumber = bigint | number;
export const BigIntOrNumberGuard = new t.Type<BigIntOrNumber, bigint, unknown>(
"BigIntOrNumber",
(u): u is BigIntOrNumber => typeof u === "bigint" || typeof u === "number",
(u, c) => {
if (typeof u === "bigint") {
return t.success(u);
} else if (typeof u === "number") {
return t.success(u);
} else {
return t.failure(u, c);
}
},
(u) => BigInt(u)
);
Loading
Loading