Skip to content

Commit

Permalink
Add delay payment creation
Browse files Browse the repository at this point in the history
  • Loading branch information
hrajchert committed Dec 15, 2023
1 parent 0380cb9 commit e53bc88
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 118 deletions.
216 changes: 109 additions & 107 deletions examples/nodejs/src/marlowe-object-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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);
Expand All @@ -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;
Expand All @@ -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";
Expand All @@ -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();
}

Expand All @@ -100,7 +143,7 @@ async function contractMenu() {
});
}

async function mainLoop() {
async function mainLoop(lifecycle: RuntimeLifecycle) {
try {
while (true) {
const action = await select({
Expand All @@ -113,7 +156,7 @@ async function mainLoop() {
});
switch (action) {
case "create":
await createContractMenu();
await createContractMenu(lifecycle);
break;
case "load":
await loadContractMenu();
Expand All @@ -130,92 +173,9 @@ async function mainLoop() {
}
}
}
// #endregion

type CliArgs = ReturnType<typeof parseCliArgs>;

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 -- <pay-to> <amount> <deadlines>"
);
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;
Expand All @@ -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",
Expand Down Expand Up @@ -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),
Expand All @@ -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
23 changes: 12 additions & 11 deletions packages/runtime/lifecycle/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -23,6 +23,7 @@ import { Next } from "@marlowe.io/language-core-v1/next";

export type RuntimeLifecycle = {
wallet: WalletAPI;
restClient: RestClient;
contracts: ContractsAPI;
payouts: PayoutsAPI;
};
Expand Down Expand Up @@ -62,15 +63,15 @@ export type CreateContractRequest = {
* <h4>Participants</h4>
* <p>
* Participants ({@link @marlowe.io/language-core-v1!index.Party | Party}) in a Marlowe Contract can be expressed in 2 ways:
*
*
* 1. **By Adressses** : When an address is fixed in the contract we don't need to provide further configuration.
* 2. **By Roles** : When the participation is done through a Role Token, we need to define if that token is minted as part of the contract creation transaction or if it was minted beforehand.
*
*
* </p>
*
* <h4>Configuration Options</h4>
* <p>
*
*
* - **When to create (mint)**
* - **Within the Runtime** : At the contrat creation, these defined Roles Tokens will be minted "on the fly" by the runtime.
* - **Without the Runtime** : before the creation, these Role Tokens are already defined (via an NFT platform, `cardano-cli`, another Marlowe Contract Created, etc.. )
Expand All @@ -79,21 +80,21 @@ export type CreateContractRequest = {
* - **Openly** (Open Roles) : Whoever applies an input (IDeposit or IChoice) on the contract `contract` first will be identified as a participant by receiving the Role Token in their wallet. In that case, participants are unknown at the creation and the participation is open to any meeting the criteria.
* - **With or without Metadata**
* - **Quantities to create(Mint)** : When asking to mint the tokens within the Runtime, quantities can defined as well.
*
* Smart Constructors are available to ease these configuration:
*
* Smart Constructors are available to ease these configuration:
* - {@link @marlowe.io/runtime-rest-client!contract.useMintedRoles}
* - {@link @marlowe.io/runtime-rest-client!contract.mintRole}
*
*
* @remarks
* - The Distribution can be a mix of Closed and Open Role Tokens configuration. See examples below.
* </p>
*
* @example
*
* ```ts
* //////////////
* //////////////
* // #1 - Mint Role Tokens
* //////////////
* //////////////
* const anAddressBech32 = "addr_test1qqe342swyfn75mp2anj45f8ythjyxg6m7pu0pznptl6f2d84kwuzrh8c83gzhrq5zcw7ytmqc863z5rhhwst3w4x87eq0td9ja"
* const aMintingConfiguration =
* { "closed_Role_A_NFT" : mintRole(anAddressBech32)
Expand Down Expand Up @@ -130,8 +131,8 @@ export type CreateContractRequest = {
* })
* }
*
* //////////////
* // #2 Use Minted Roles Tokens
* //////////////
* // #2 Use Minted Roles Tokens
* const aUseMintedRoleTokensConfiguration =
* useMintedRoles(
* "e68f1cea19752d1292b4be71b7f5d2b3219a15859c028f7454f66cdf",
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/lifecycle/src/generic/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function mkRuntimeLifecycle(
): RuntimeLifecycle {
return {
wallet: wallet,
restClient,
contracts: mkContractLifecycle(wallet, deprecatedRestAPI, restClient),
payouts: mkPayoutLifecycle(wallet, deprecatedRestAPI, restClient),
};
Expand Down

0 comments on commit e53bc88

Please sign in to comment.