From 5ae9b9c0b6350a391f176c3030ab5c999d40fde2 Mon Sep 17 00:00:00 2001 From: nhenin Date: Thu, 30 Mar 2023 14:51:30 +0400 Subject: [PATCH 1/3] [test] add environment variables --- env/.env.test | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 env/.env.test diff --git a/env/.env.test b/env/.env.test new file mode 100644 index 00000000..c8a70f4e --- /dev/null +++ b/env/.env.test @@ -0,0 +1,5 @@ +MARLOWE_WEB_SERVER_URL="http://0.0.0.0:32963" +BLOCKFROST_PROJECT_ID="" +BLOCKFROST_URL="https://cardano-preview.blockfrost.io/api/v0" +NETWORK_ID=Preview +BANK_PK_HEX='' \ No newline at end of file From 7e460c4193badb629d19d94c2706cc5238450264 Mon Sep 17 00:00:00 2001 From: nhenin Date: Thu, 30 Mar 2023 14:53:07 +0400 Subject: [PATCH 2/3] add gitignore --- .gitignore | 21 +++++++++++++++++++++ env/.env.test | 5 ----- 2 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 .gitignore delete mode 100644 env/.env.test diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a6a6b3e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +coverage +node_modules +output +docs +env +tsconfig.tsbuildinfo +lerna-debug.log +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# created by nix +result +source +dist + +package-lock.json +yarn.lock \ No newline at end of file diff --git a/env/.env.test b/env/.env.test deleted file mode 100644 index c8a70f4e..00000000 --- a/env/.env.test +++ /dev/null @@ -1,5 +0,0 @@ -MARLOWE_WEB_SERVER_URL="http://0.0.0.0:32963" -BLOCKFROST_PROJECT_ID="" -BLOCKFROST_URL="https://cardano-preview.blockfrost.io/api/v0" -NETWORK_ID=Preview -BANK_PK_HEX='' \ No newline at end of file From bb353f43c46f59a7138f6ea2e0931d12ccd397b6 Mon Sep 17 00:00:00 2001 From: nhenin Date: Thu, 30 Mar 2023 15:03:07 +0400 Subject: [PATCH 3/3] Big Bang : Marlowe Language / Runtime --- .vscode/launch.json | 31 ++ dotenv/dotenv-test.js | 7 + fixup | 11 + flake.lock | 78 ----- flake.nix | 63 ---- jest.config.console.js | 1 + jest.config.js | 55 ++++ package.json | 81 ++++- src/adapter/file.ts | 10 + src/adapter/json.ts | 23 ++ src/adapter/logging.ts | 5 + src/adapter/wallet/ada.ts | 4 + src/adapter/wallet/lucid.ts | 183 +++++++++++ src/adapter/wallet/privateKeys.ts | 2 + src/index.ts | 300 ------------------ .../core/v1/examples/contract-one-notify.ts | 19 ++ src/language/core/v1/examples/index.ts | 2 + src/language/core/v1/examples/swap.ts | 117 +++++++ .../core/v1/semantics/contract/assert.ts | 12 + .../core/v1/semantics/contract/close.ts | 5 + .../v1/semantics/contract/common/address.ts | 7 + .../semantics/contract/common/observations.ts | 75 +++++ .../contract/common/payee/account.ts | 12 + .../semantics/contract/common/payee/index..ts | 9 + .../semantics/contract/common/payee/party.ts | 20 ++ .../v1/semantics/contract/common/policyId.ts | 4 + .../v1/semantics/contract/common/token.ts | 12 + .../v1/semantics/contract/common/value.ts | 91 ++++++ src/language/core/v1/semantics/contract/if.ts | 14 + .../core/v1/semantics/contract/index.ts | 28 ++ .../core/v1/semantics/contract/let.ts | 14 + .../core/v1/semantics/contract/pay.ts | 31 ++ .../semantics/contract/when/action/choice.ts | 20 ++ .../semantics/contract/when/action/deposit.ts | 18 ++ .../semantics/contract/when/action/index.ts | 22 ++ .../semantics/contract/when/action/notify.ts | 8 + .../core/v1/semantics/contract/when/index.ts | 31 ++ .../semantics/contract/when/input/choice.ts | 10 + .../semantics/contract/when/input/deposit.ts | 11 + .../v1/semantics/contract/when/input/index.ts | 30 ++ .../semantics/contract/when/input/notify.ts | 6 + src/language/index.ts | 1 + src/language/legacy/dsl.ts | 288 +++++++++++++++++ .../legacy/examples/contractForDifferences.ts | 217 +++++++++++++ .../contractForDifferencesWithOracle.ts | 235 ++++++++++++++ .../legacy/examples/couponBondGuaranteed.ts | 124 ++++++++ src/language/legacy/examples/escrow.ts | 100 ++++++ .../legacy/examples/escrowWithCollateral.ts | 140 ++++++++ src/language/legacy/examples/index.ts | 7 + src/language/legacy/examples/swap.ts | 89 ++++++ .../legacy/examples/zeroCouponBond.ts | 50 +++ src/runtime/common/address.ts | 8 + src/runtime/common/block.ts | 25 ++ src/runtime/common/codec.ts | 1 + src/runtime/common/http.ts | 26 ++ src/runtime/common/iso8601.ts | 10 + src/runtime/common/metadata/index.ts | 17 + src/runtime/common/metadata/tag.ts | 9 + src/runtime/common/policyId.ts | 8 + src/runtime/common/state.ts | 9 + src/runtime/common/textEnvelope.ts | 15 + src/runtime/common/tx/id.ts | 4 + src/runtime/common/tx/outRef.ts | 4 + src/runtime/common/version.ts | 5 + src/runtime/common/wallet.ts | 12 + src/runtime/contract/details.ts | 34 ++ src/runtime/contract/endpoints/collection.ts | 106 +++++++ src/runtime/contract/endpoints/singleton.ts | 45 +++ src/runtime/contract/header.ts | 21 ++ src/runtime/contract/id.ts | 20 ++ src/runtime/contract/role.ts | 42 +++ src/runtime/contract/transaction/details.ts | 38 +++ .../transaction/endpoints/collection.ts | 106 +++++++ .../transaction/endpoints/singleton.ts | 42 +++ src/runtime/contract/transaction/header.ts | 24 ++ src/runtime/contract/transaction/id.ts | 17 + src/runtime/contract/transaction/status.ts | 4 + src/runtime/contract/withdrawal/details.ts | 28 ++ .../withdrawal/endpoints/collection.ts | 100 ++++++ .../withdrawal/endpoints/singleton.ts | 44 +++ src/runtime/contract/withdrawal/header.ts | 16 + src/runtime/contract/withdrawal/id.ts | 16 + src/runtime/endpoints.ts | 76 +++++ src/runtime/index.ts | 2 + src/runtime/write/command.ts | 74 +++++ src/tsconfig.json | 5 +- test/global.d.ts | 3 + .../jsons/contractForDifferences.json | 219 +++++++++++++ .../contractForDifferencesWithOracle.json | 198 ++++++++++++ .../examples/jsons/couponBondGuaranteed.json | 237 ++++++++++++++ .../core/v1/examples/jsons/escrow.json | 146 +++++++++ .../examples/jsons/escrowWithCollateral.json | 167 ++++++++++ .../language/core/v1/examples/jsons/swap.json | 75 +++++ .../v1/examples/jsons/zeroCouponBond.json | 75 +++++ .../language/core/v1/examples/parsing.spec.ts | 44 +++ .../accounts/jsons/one-account-with-ada.json | 14 + .../common/payee/accounts/parsing.spec.ts | 38 +++ .../contract/common/value/jsons/value.json | 24 ++ .../contract/common/value/parsing.spec.ts | 37 +++ .../v1/semantics/contract/jsons/close.json | 1 + .../core/v1/semantics/contract/jsons/let.json | 6 + .../core/v1/semantics/contract/jsons/pay.json | 16 + .../v1/semantics/contract/jsons/when.json | 5 + .../v1/semantics/contract/parsing.spec.ts | 45 +++ .../contract/when/action/jsons/deposit.json | 16 + .../contract/when/action/parsing.spec.ts | 37 +++ test/runtime/context.ts | 21 ++ test/runtime/endpoints/contracts.spec.e2e.ts | 83 +++++ .../endpoints/transactions.spec.e2e.ts | 116 +++++++ .../runtime/endpoints/withdrawals.spec.e2e.ts | 108 +++++++ test/runtime/examples/swap.spec.e2e.ts | 67 ++++ test/runtime/provisionning.ts | 113 +++++++ test/tsconfig.json | 8 + tsconfig-base.json | 31 ++ tsconfig-cjs.json | 8 + tsconfig.json | 105 +----- 116 files changed, 5261 insertions(+), 548 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 dotenv/dotenv-test.js create mode 100755 fixup delete mode 100644 flake.lock delete mode 100644 flake.nix create mode 100644 jest.config.console.js create mode 100644 jest.config.js create mode 100644 src/adapter/file.ts create mode 100644 src/adapter/json.ts create mode 100644 src/adapter/logging.ts create mode 100644 src/adapter/wallet/ada.ts create mode 100644 src/adapter/wallet/lucid.ts create mode 100644 src/adapter/wallet/privateKeys.ts delete mode 100644 src/index.ts create mode 100644 src/language/core/v1/examples/contract-one-notify.ts create mode 100644 src/language/core/v1/examples/index.ts create mode 100644 src/language/core/v1/examples/swap.ts create mode 100644 src/language/core/v1/semantics/contract/assert.ts create mode 100644 src/language/core/v1/semantics/contract/close.ts create mode 100644 src/language/core/v1/semantics/contract/common/address.ts create mode 100644 src/language/core/v1/semantics/contract/common/observations.ts create mode 100644 src/language/core/v1/semantics/contract/common/payee/account.ts create mode 100644 src/language/core/v1/semantics/contract/common/payee/index..ts create mode 100644 src/language/core/v1/semantics/contract/common/payee/party.ts create mode 100644 src/language/core/v1/semantics/contract/common/policyId.ts create mode 100644 src/language/core/v1/semantics/contract/common/token.ts create mode 100644 src/language/core/v1/semantics/contract/common/value.ts create mode 100644 src/language/core/v1/semantics/contract/if.ts create mode 100644 src/language/core/v1/semantics/contract/index.ts create mode 100644 src/language/core/v1/semantics/contract/let.ts create mode 100644 src/language/core/v1/semantics/contract/pay.ts create mode 100644 src/language/core/v1/semantics/contract/when/action/choice.ts create mode 100644 src/language/core/v1/semantics/contract/when/action/deposit.ts create mode 100644 src/language/core/v1/semantics/contract/when/action/index.ts create mode 100644 src/language/core/v1/semantics/contract/when/action/notify.ts create mode 100644 src/language/core/v1/semantics/contract/when/index.ts create mode 100644 src/language/core/v1/semantics/contract/when/input/choice.ts create mode 100644 src/language/core/v1/semantics/contract/when/input/deposit.ts create mode 100644 src/language/core/v1/semantics/contract/when/input/index.ts create mode 100644 src/language/core/v1/semantics/contract/when/input/notify.ts create mode 100644 src/language/index.ts create mode 100644 src/language/legacy/dsl.ts create mode 100644 src/language/legacy/examples/contractForDifferences.ts create mode 100644 src/language/legacy/examples/contractForDifferencesWithOracle.ts create mode 100644 src/language/legacy/examples/couponBondGuaranteed.ts create mode 100644 src/language/legacy/examples/escrow.ts create mode 100644 src/language/legacy/examples/escrowWithCollateral.ts create mode 100644 src/language/legacy/examples/index.ts create mode 100644 src/language/legacy/examples/swap.ts create mode 100644 src/language/legacy/examples/zeroCouponBond.ts create mode 100644 src/runtime/common/address.ts create mode 100644 src/runtime/common/block.ts create mode 100644 src/runtime/common/codec.ts create mode 100644 src/runtime/common/http.ts create mode 100644 src/runtime/common/iso8601.ts create mode 100644 src/runtime/common/metadata/index.ts create mode 100644 src/runtime/common/metadata/tag.ts create mode 100644 src/runtime/common/policyId.ts create mode 100644 src/runtime/common/state.ts create mode 100644 src/runtime/common/textEnvelope.ts create mode 100644 src/runtime/common/tx/id.ts create mode 100644 src/runtime/common/tx/outRef.ts create mode 100644 src/runtime/common/version.ts create mode 100644 src/runtime/common/wallet.ts create mode 100644 src/runtime/contract/details.ts create mode 100644 src/runtime/contract/endpoints/collection.ts create mode 100644 src/runtime/contract/endpoints/singleton.ts create mode 100644 src/runtime/contract/header.ts create mode 100644 src/runtime/contract/id.ts create mode 100644 src/runtime/contract/role.ts create mode 100644 src/runtime/contract/transaction/details.ts create mode 100644 src/runtime/contract/transaction/endpoints/collection.ts create mode 100644 src/runtime/contract/transaction/endpoints/singleton.ts create mode 100644 src/runtime/contract/transaction/header.ts create mode 100644 src/runtime/contract/transaction/id.ts create mode 100644 src/runtime/contract/transaction/status.ts create mode 100644 src/runtime/contract/withdrawal/details.ts create mode 100644 src/runtime/contract/withdrawal/endpoints/collection.ts create mode 100644 src/runtime/contract/withdrawal/endpoints/singleton.ts create mode 100644 src/runtime/contract/withdrawal/header.ts create mode 100644 src/runtime/contract/withdrawal/id.ts create mode 100644 src/runtime/endpoints.ts create mode 100644 src/runtime/index.ts create mode 100644 src/runtime/write/command.ts create mode 100644 test/global.d.ts create mode 100644 test/language/core/v1/examples/jsons/contractForDifferences.json create mode 100644 test/language/core/v1/examples/jsons/contractForDifferencesWithOracle.json create mode 100644 test/language/core/v1/examples/jsons/couponBondGuaranteed.json create mode 100644 test/language/core/v1/examples/jsons/escrow.json create mode 100644 test/language/core/v1/examples/jsons/escrowWithCollateral.json create mode 100644 test/language/core/v1/examples/jsons/swap.json create mode 100644 test/language/core/v1/examples/jsons/zeroCouponBond.json create mode 100644 test/language/core/v1/examples/parsing.spec.ts create mode 100644 test/language/core/v1/semantics/contract/common/payee/accounts/jsons/one-account-with-ada.json create mode 100644 test/language/core/v1/semantics/contract/common/payee/accounts/parsing.spec.ts create mode 100644 test/language/core/v1/semantics/contract/common/value/jsons/value.json create mode 100644 test/language/core/v1/semantics/contract/common/value/parsing.spec.ts create mode 100644 test/language/core/v1/semantics/contract/jsons/close.json create mode 100644 test/language/core/v1/semantics/contract/jsons/let.json create mode 100644 test/language/core/v1/semantics/contract/jsons/pay.json create mode 100644 test/language/core/v1/semantics/contract/jsons/when.json create mode 100644 test/language/core/v1/semantics/contract/parsing.spec.ts create mode 100644 test/language/core/v1/semantics/contract/when/action/jsons/deposit.json create mode 100644 test/language/core/v1/semantics/contract/when/action/parsing.spec.ts create mode 100644 test/runtime/context.ts create mode 100644 test/runtime/endpoints/contracts.spec.e2e.ts create mode 100644 test/runtime/endpoints/transactions.spec.e2e.ts create mode 100644 test/runtime/endpoints/withdrawals.spec.e2e.ts create mode 100644 test/runtime/examples/swap.spec.e2e.ts create mode 100644 test/runtime/provisionning.ts create mode 100644 test/tsconfig.json create mode 100644 tsconfig-base.json create mode 100644 tsconfig-cjs.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..c6f80079 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug Tests", + "program": "${workspaceRoot}/node_modules/.bin/jest", + "cwd": "${workspaceRoot}", + "args": ["--i", "--config", "jest.config.js"], + }, + { + "name": "Launch Extension (development)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "preLaunchTask": "watch" + }, + { + "name": "Launch Extension (production)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "preLaunchTask": "npm: compile" + } + ] + } \ No newline at end of file diff --git a/dotenv/dotenv-test.js b/dotenv/dotenv-test.js new file mode 100644 index 00000000..0c8b795d --- /dev/null +++ b/dotenv/dotenv-test.js @@ -0,0 +1,7 @@ +/* eslint-disable import/no-extraneous-dependencies */ +const path = require('path'); +const dotenv = require('dotenv'); + +module.exports = async () => { + dotenv.config({ path: path.resolve(__dirname, '../env/.env.test') }); +}; \ No newline at end of file diff --git a/fixup b/fixup new file mode 100755 index 00000000..73070169 --- /dev/null +++ b/fixup @@ -0,0 +1,11 @@ +cat >dist/cjs/package.json <dist/mjs/package.json </src/adapter/$1', + '@runtime/(.*)': '/src/runtime/$1', + '@language/(.*)': '/src/language/$1', + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + globalSetup: "./dotenv/dotenv-test.js", + setupFilesAfterEnv: ["./jest.config.console.js"], + transform: { + // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest` + // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest` + '^.+\\.m?[tj]sx?$': [ + 'ts-jest', + { + useESM: true, + }, + ], + }, + }, + { + displayName: "unit test", + testMatch: ["./**/*.spec.ts"], + extensionsToTreatAsEsm: ['.ts'], + preset: 'ts-jest/presets/default-esm', + moduleNameMapper: { + '@adapter/(.*)': '/src/adapter/$1', + '@runtime/(.*)': '/src/runtime/$1', + '@language/(.*)': '/src/language/$1', + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + globalSetup: "./dotenv/dotenv-test.js", + setupFilesAfterEnv: ["./jest.config.console.js"], + transform: { + // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest` + // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest` + '^.+\\.m?[tj]sx?$': [ + 'ts-jest', + { + useESM: true, + }, + ], + }, + } + ] +}; + diff --git a/package.json b/package.json index a84365e3..0cb9a8d4 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,82 @@ { + "name": "marlowe-ts-sdk", + "version": "0.0.1", + "description": "Marlowe Runtime Client for building Marlowe Contracts", + "engines": { + "node": ">=14.20.1" + }, + "repository": "https://github.com/input-output-hk/marlowe-ts-sdk", + "publishConfig": { + "access": "public" + }, + "contributors": [ + "Nicolas Henin (https://iohk.io)" + ], + "license": "Apache-2.0", + "main": "dist/cjs/index.js", + "module": "dist/mjs/index.js", + "exports": { + ".": { + "import": "./dist/mjs/index.js", + "require": "./dist/cjs/index.js" + } + }, + "scripts": { + "build:esm": "tsc -p src/tsconfig.json --outDir ./dist/esm --module es2020", + "build:cjs": "tsc --build src", + "build": "rm -fr dist/* && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && ./fixup", + "circular-deps:check": "madge --circular dist", + "tscNoEmit": "shx echo typescript --noEmit command not implemented yet", + "cleanup:dist": "shx rm -rf dist", + "cleanup:nm": "shx rm -rf node_modules", + "cleanup": "run-s cleanup:dist cleanup:nm", + "lint": "eslint -c ../../complete.eslintrc.js \"src/**/*.ts\" \"test/**/*.ts\"", + "lint:fix": "yarn lint --fix", + "test": "yarn node --experimental-vm-modules $(yarn bin jest -c ./jest.config.js)", + "test:build:verify": "tsc --build ./test", + "test:e2e": "shx echo 'test:e2e' command not implemented yet", + "coverage": "shx echo No coverage report for this package", + "prepack": "yarn build" + }, + "devDependencies": { + "@types/axios-curlirize": "^1.3.2", + "@types/deep-equal": "^1.0.1", + "@types/jest": "^26.0.24", + "@types/json-bigint": "^1.0.1", + "@types/node": "^18.14.2", + "eslint": "^7.32.0", + "jest": "^29.4", + "npm-run-all": "^4.1.5", + "prettier": "^2.3.2", + "shx": "^0.3.3", + "ts-jest": "^29.0.5", + "jest-serial-runner": "^1.2.1", + "@relmify/jest-fp-ts": "^2.0.2", + "ts-node": "^10.9.1" + }, "dependencies": { + "@blockfrost/blockfrost-js": "^5.2.0", + "@blockfrost/openapi": "^0.1.54", + "@types/deep-equal": "^1.0.1", "axios": "^1.3.3", + "axios-curlirize": "^2.0.0", + "date-fns": "2.29.3", + "deep-equal": "^1.0.1", + "dotenv": "^16.0.3", + "fp-ts": "^2.13.1", + "json-bigint": "^1.0.0", + "lucid-cardano": "0.9.4", + "ts-adt": "^2.0.2", + "ts-pattern": "^4.2.0", + "retry-ts":"0.1.4", + "newtype-ts":"0.3.5", + "monocle-ts":"2.3.13", + "io-ts":"2.2.20", + "io-ts-types":"0.5.19", + "io-ts-bigint":"2.0.1", + "io-ts-reporters":"2.0.1", "typescript": "^4.9.5", "typescript-language-server": "^3.1.0" }, - "devDependencies": { - "openapi-typescript": "^6.1.0", - "openapi-typescript-codegen": "^0.23.0", - "ts-node": "^10.9.1" - }, - "type": "module" + "packageManager": "yarn@3.2.1" } diff --git a/src/adapter/file.ts b/src/adapter/file.ts new file mode 100644 index 00000000..e9ab41f4 --- /dev/null +++ b/src/adapter/file.ts @@ -0,0 +1,10 @@ + +import * as TE from 'fp-ts/TaskEither' +import * as E from 'fp-ts/Either' + +import fs from 'fs'; +import { promisify } from 'util'; + + +const readFromFile = promisify(fs.readFile); +export const getFileContents = (path: string) => TE.tryCatch(() => readFromFile(path, 'utf-8'), E.toError); diff --git a/src/adapter/json.ts b/src/adapter/json.ts new file mode 100644 index 00000000..84d71f4e --- /dev/null +++ b/src/adapter/json.ts @@ -0,0 +1,23 @@ +import * as t from "io-ts"; +import * as C from 'io-ts/Codec' +import * as D from 'io-ts/Decoder' +import * as E from 'io-ts/Encoder' +import JSONbig from 'json-bigint'; //(({useNativeBigInt:false})) + +const JsonAlwayAndOnlyBigInt = JSONbig ({ + alwaysParseAsBig: true, + useNativeBigInt: true, + }); + + +export const MarloweJSONDecoder: D.Decoder = { + decode: (data) => (data === ''? null : JsonAlwayAndOnlyBigInt.parse(data)) +} + +export const MarloweJSONEncoder: E.Encoder = { + encode: (contract) => JsonAlwayAndOnlyBigInt.stringify(contract) +} + +export const MarloweJSONCodec: C.Codec = C.make(MarloweJSONDecoder, MarloweJSONEncoder) + +export const minify = (a:string) => a.replace(/[\n\r\s]/g, '') \ No newline at end of file diff --git a/src/adapter/logging.ts b/src/adapter/logging.ts new file mode 100644 index 00000000..17ef79dd --- /dev/null +++ b/src/adapter/logging.ts @@ -0,0 +1,5 @@ + +export function log (message:string) { + console.log(`\t## - ${message}`); + } + diff --git a/src/adapter/wallet/ada.ts b/src/adapter/wallet/ada.ts new file mode 100644 index 00000000..2021d891 --- /dev/null +++ b/src/adapter/wallet/ada.ts @@ -0,0 +1,4 @@ + + +export const format = (lovelaces:BigInt): String => new Intl.NumberFormat().format((lovelaces.valueOf() / 1_000_000n)).concat(" ₳"); + diff --git a/src/adapter/wallet/lucid.ts b/src/adapter/wallet/lucid.ts new file mode 100644 index 00000000..d55af1bb --- /dev/null +++ b/src/adapter/wallet/lucid.ts @@ -0,0 +1,183 @@ +import { pipe } from 'fp-ts/function'; +import * as A from 'fp-ts/Array'; +import * as API from '@blockfrost/blockfrost-js' +import { Blockfrost, Lucid, C, Network, PrivateKey, PolicyId, getAddressDetails, toUnit, fromText, NativeScript, Tx ,Core, TxSigned, TxComplete, Script, fromHex, toHex } from 'lucid-cardano'; +import * as O from 'fp-ts/Option' +import { log } from '../logging' +import * as TE from 'fp-ts/TaskEither' +import * as T from 'fp-ts/Task' +import { addressBech32, AddressBech32, unAddressBech32 } from '@runtime/common/address'; +import { HexTransactionWitnessSet , MarloweTxCBORHex} from '@runtime/common/textEnvelope'; + +export class Asset { + policyId:string; + tokenName:string; + + public constructor(policyId:string,tokenName:string){ + this.policyId = policyId; + this.tokenName = tokenName; + } + + public toString : () => string = () => `${this.policyId}|${this.tokenName}` +} +export type Address = string; + +export class Context { + projectId:string; + network:Network; + blockfrostUrl:string + + public constructor (projectId:string, blockfrostUrl:string,network:Network, ) { + this.projectId = projectId; + this.network = network; + this.blockfrostUrl = blockfrostUrl; + } +} + +export const getPrivateKeyFromHexString = (privateKeyHex:string) : PrivateKey => C.PrivateKey.from_bytes(Buffer.from(privateKeyHex, 'hex')).to_bech32() + +export class SingleAddressWallet { + private privateKeyBech32: string; + private context:Context; + private lucid : Lucid; + private blockfrostApi: API.BlockFrostAPI; + + public address : AddressBech32; + + private constructor (context:Context,privateKeyBech32:PrivateKey) { + this.privateKeyBech32 = privateKeyBech32; + this.context = context; + this.blockfrostApi = new API.BlockFrostAPI({projectId: context.projectId}); + } + + public static Initialise ( context:Context, privateKeyBech32: string) : T.Task { + const account = new SingleAddressWallet(context,privateKeyBech32); + return (() => account.initialise().then(() => account)); + } + + public static Random ( context:Context) : T.Task { + const privateKey = C.PrivateKey.generate_ed25519().to_bech32(); + const account = new SingleAddressWallet(context,privateKey); + return (() => account.initialise().then(() => account)); + } + + private async initialise () { + this.lucid = await Lucid.new(new Blockfrost(this.context.blockfrostUrl, this.context.projectId),this.context.network); + this.lucid.selectWalletFromPrivateKey(this.privateKeyBech32); + this.address = addressBech32(await this.lucid.wallet.address ()); + } + + public adaBalance : TE.TaskEither + = pipe( TE.tryCatch( + () => this.blockfrostApi.addresses(unAddressBech32(this.address)), + (reason) => new Error(`Error while retrieving assetBalance : ${reason}`)) + , TE.map( (content) => pipe(content.amount??[] + , A.filter((amount) => amount.unit === "lovelace") + , A.map((amount) => BigInt(amount.quantity)) + , A.head + , O.getOrElse(() => 0n)))) + + + public assetBalance : (asset:Asset) => TE.TaskEither + = (asset) => + pipe(TE.tryCatch( + () => this.blockfrostApi.addresses(unAddressBech32(this.address)), + (reason) => new Error(`Error while retrieving assetBalance : ${reason}`)) + , TE.map( (content) => pipe(content.amount??[] + , A.filter((amount) => amount.unit === toUnit(asset.policyId, fromText(asset.tokenName))) + , A.map((amount) => BigInt(amount.quantity)) + , A.head + , O.getOrElse(() => 0n)))) + + + public provision : (provisionning: [SingleAddressWallet,bigint][]) => TE.TaskEither = (provisionning) => + pipe ( provisionning + , A.reduce ( this.lucid.newTx() + , (tx:Tx, account: [SingleAddressWallet,bigint]) => tx.payToAddress(unAddressBech32(account[0].address), { lovelace:account[1]})) + , build + , TE.chain(this.signSubmitAndWaitConfirmation)) + + public randomPolicyId() : [Script,PolicyId] { + const { paymentCredential } = getAddressDetails(unAddressBech32(this.address)); + const before = this.lucid.currentSlot() + (5 * 60) + const json : NativeScript = { + type: "all", + scripts: [ + { + type: "before", + slot: before.valueOf(), + }, + { type: "sig", keyHash: paymentCredential?.hash! } + ], + }; + const script = this.lucid.utils.nativeScriptFromJson(json); + const policyId = this.lucid.utils.mintingPolicyToId(script); + return [script,policyId]; + } + + public mintRandomTokens(tokenName:string, amount: BigInt) : TE.TaskEither { + const policyRefs = this.randomPolicyId (); + const { paymentCredential } = getAddressDetails(unAddressBech32(this.address)); + const before = this.lucid.currentSlot() + (5 * 60) + const validTo = this.lucid.currentSlot() + 60 + const [mintingPolicy,policyId] = policyRefs + return pipe( this.lucid.newTx() + .mintAssets({[toUnit(policyId, fromText(tokenName))]: amount.valueOf()}) + .validTo(Date.now() + 100000) + .attachMintingPolicy(mintingPolicy) + , build + , TE.chain(this.signSubmitAndWaitConfirmation) + , TE.map(() => new Asset (policyRefs[1],tokenName))) + } + + public signMarloweTx : (cborHex :MarloweTxCBORHex) => TE.TaskEither + = (cborHex) => + pipe ( this.fromTxCBOR(cborHex) + , this.signTx + , TE.map((txSigned) => toHex(txSigned.to_bytes()))) + + private fromTxCBOR (cbor : string) : Core.Transaction { + return C.Transaction.from_bytes(fromHex(cbor)) + } + private signTx : (tx : Core.Transaction) => TE.TaskEither + = (tx) => + TE.tryCatch( + () => this.lucid.wallet.signTx(tx), + (reason) => new Error(`Error while signing : ${reason}`)); + + public sign : (txBuilt : TxComplete ) => TE.TaskEither + = (txBuilt) => + TE.tryCatch( + () => txBuilt.sign().complete(), + (reason) => new Error(`Error while signing : ${reason}`)); + + + public submit : (signedTx : TxSigned ) => TE.TaskEither + = (signedTx) => + TE.tryCatch( + () => signedTx.submit(), + (reason) => new Error(`Error while submitting : ${reason}`)); + + public waitConfirmation : (txHash : string ) => TE.TaskEither + = (txHash) => + TE.tryCatch( + () => this.lucid.awaitTx(txHash), + (reason) => new Error(`Error while submitting : ${reason}`)); + + public signSubmitAndWaitConfirmation : (txBuilt : TxComplete) => TE.TaskEither + = (txBuilt) => + pipe(this.sign(txBuilt) + ,TE.chain(this.submit) + ,TE.chainFirst((txHash) => TE.of(log(`<> Tx ${txHash} submitted.`))) + ,TE.chain(this.waitConfirmation)) + + +} + +const build : (tx : Tx ) => TE.TaskEither + = (tx) => TE.tryCatch( + () => tx.complete(), + (reason) => new Error(`Error while building Tx : ${reason}`)); + + + diff --git a/src/adapter/wallet/privateKeys.ts b/src/adapter/wallet/privateKeys.ts new file mode 100644 index 00000000..1ad1b688 --- /dev/null +++ b/src/adapter/wallet/privateKeys.ts @@ -0,0 +1,2 @@ + +export type PrivateKeysAsHex = string \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 6316179d..00000000 --- a/src/index.ts +++ /dev/null @@ -1,300 +0,0 @@ -import axios, { AxiosInstance } from 'axios'; - -// API responses brings us back URLs so we are encouraged to not construct them manually. -// We use a opaque string to represent URLs for that. -// -// Opaque types idiom (like a hidden `newtype` constructor in Haskell) -declare const ContractsEndpoint: unique symbol; - -type ContractsEndpoint = string & {_opaque: typeof ContractsEndpoint}; - -declare const ContractEndpoint: unique symbol; - -// It seems like overengeeniring to have a root endpoint defined like this -// but we gonna extend the API and it gonna be served as a part of root response. -export const contractsEndpoint = "/contracts" as ContractsEndpoint; - -type ContractEndpoint = string & {_opaque: typeof ContractEndpoint}; - -// We are cheating in here a bit by hardcoding URLs ;-) -export const contractEndpoint = (contractId: string) => `/contracts/${contractId}` as ContractEndpoint; - -declare const TransactionsEndpoint: unique symbol; - -type TransactionsEndpoint = string & {_opaque: typeof TransactionsEndpoint}; - -export const transactionsEndpoint = (contractId: string) => `/contracts/${contractId}/transactions` as TransactionsEndpoint; - -declare const TransactionEndpoint: unique symbol; - -type TransactionEndpoint = string & {_opaque: typeof TransactionEndpoint}; - -export const transactionEndpoint = (contractId: string, transactionId: string) => - `/contracts/${contractId}/transactions/${transactionId}` as TransactionEndpoint; - -type Bech32 = string; - -type Version = "v1"; - -type Metadata = Record; - -// Just a stub for Marlowe Contract and State -type Contract = "close"; -type State = any; -type Input = "input_notify"; - -// Currently the runtime API doesn't provide any additional information -// beside the error status code like 400, 404, 500 etc. -type ErrorResponse = number; - -type TxOutRef = string; -type PolicyId = string; -type TxStatus = "unsigned" | "submitted" | "confirmed"; - -interface BlockHeader { - slotNo: number, // These should be BigInts - blockNo: number, - blockHeaderHash: string -} - -interface ContractHeader { - contractId: TxOutRef, - roleTokenMintingPolicyId: PolicyId, - version: Version, - metadata?: Record // FIXME - status: TxStatus - blockHeader?: BlockHeader -} - -interface ContractHeaderItem { - results: ContractHeader; - links: { contract: ContractEndpoint } -} - -declare const ContractsRange: unique symbol; - -type ContractsRange = string & {_opaque: typeof ContractsRange}; - -interface IndexResponse { - nextRange?: Range, - prevRange?: Range, - items: Response[] -} - -export type GetContractsResponse = IndexResponse; - -export interface PostContractsRequest { - contract: Contract, - roles?: any // RolesConfig - version?: Version, - metadata?: Metadata, - minUTxODeposit: number, - changeAddress: Bech32, - addresses?: Bech32[], // When skipped we use `[changeAddress]` - collateralUTxOs?: Bech32[] -} - - -export interface PostContractsResponse { - contractId: TxOutRef, - endpoint: ContractEndpoint, - // This contains a CBOR of the `TxBody`. The REST API gonna be extended so - // we can also fetch a whole Transaction (CIP-30 `signTx` expects actually a whole `Tx`). - txBody: TextEnvelope -} - -interface TextEnvelope { - type: string, - description?: string, - cborHex: string -}; - -interface ContractState extends ContractHeader { - initialContract: Contract - currentContract?: Contract - state?: State - utxo?: TxOutRef - txBody?: TextEnvelope -} - -type ISO8601 = string; - -export interface PostTransactionsRequest { - inpts: Input[], - invalidBefore: ISO8601, - invalidHereafter: ISO8601, - metadata?: Metadata, - changeAddress: Bech32, - addresses?: Bech32[], // When skipped we use `[changeAddress]` - collateralUTxOs?: Bech32[], -} - -type TxId = string; - -export interface TxHeader { - contractId: TxOutRef, - transactionId: TxId, - status: TxStatus, - block?: BlockHeader, - utxo?: TxOutRef -} - -interface TxHeaderItem { - results: TxHeader; - links: { contract: TransactionEndpoint } -} - -export interface GetTransactionsRequestOptions { - range?: string -} - -declare const TransactionsRange: unique symbol; - -type TransactionsRange = string & {_opaque: typeof TransactionsRange}; - -export type GetTransactionsResponse = IndexResponse; - -export interface MarloweRuntimeApi { - contracts: { - get: ( - route: ContractsEndpoint, - range?: ContractsRange - ) => Promise; - post: ( - route: ContractsEndpoint, - input: PostContractsRequest - ) => Promise; - }, - contract: { - get: (route: ContractEndpoint) => Promise - put: (route: ContractEndpoint, input: TextEnvelope) => Promise - }, - transactions: { - get: (route: TransactionsEndpoint) => Promise - } -}; - -export function MarloweRuntimeClient(request: AxiosInstance): MarloweRuntimeApi { - return { - contracts: { - get: async (route: ContractsEndpoint, range?: ContractsRange): Promise => { - const config = range?{ headers: { Range: range as string } } : {}; - - return request.get(route as string, config) - .then(response => { - return { - items: response.data.results, - nextRange: response.headers["next-range"] as ContractsRange, - prevRange: response.headers["prev-range"] as ContractsRange - }; - }) - .catch(error => error.status); - }, - post: async (route: ContractsEndpoint, input: PostContractsRequest): Promise => { - const data = { - contract: input.contract, - roles: input.roles ?? null, - version: input.version ?? "v1", - metadata: input.metadata ?? {}, - minUTxODeposit: input.minUTxODeposit, - }; - const config = { - headers: { - "X-Change-Address": input.changeAddress, - "X-Address": (input.addresses??[input.changeAddress]).join(","), - ... input.collateralUTxOs && { "X-Collateral-UTxOs": input.collateralUTxOs}, - } - }; - return request.post(route as string, data, config).then(response => { - return { - contractId: response.data.resource.contractId, - txBody: response.data.resource.txBody, - endpoint: response.data.links.contract - }; - }).catch(error => error.status); - } - }, - contract: { - get: async (route: ContractEndpoint): Promise => { - return request.get(route as string).then(response => { - // We are getting back { links: {}, resource: contractState } here - return response.data.resource; - }).catch(error => error.status); - }, - put: async (route: ContractEndpoint, input: TextEnvelope): Promise => { - return request.post(route as string, input).then(response => - response.data.links.transactions - ).catch(error => error.status); - } - }, - transactions: { - get: async (route: TransactionsEndpoint, range?: TransactionsRange): Promise => { - const config = range?{ headers: { Range: range as string } } : {}; - - return request.get(route as string, config) - .then(response => { - return { - items: response.data.results, - nextRange: response.headers["next-range"], - prevRange: response.headers["prev-range"] - }; - }) - .catch(error => error.status); - } - } - } -} - -// Just a temporary quick and dirty tests of the client -const port = process.argv[2]; - -if(!port) { - console.error("Please provide a runtime port number as an argument"); - process.exit(1); -} - -const axiosRequest = axios.create({ - baseURL: `http://0.0.0.0:${port}`, - headers: { ContentType: 'application/json', Accept: 'application/json' } -}); - -const client = MarloweRuntimeClient(axiosRequest); - -// Ugly fetcher for all the contracts -const foldContracts = async (filterItem: ((item: ContractHeaderItem) => boolean)) => { - let result:ContractHeaderItem[] = []; - let step = async function(response: GetContractsResponse | ErrorResponse) { - if(typeof response === "number") { - console.log("Error: ", response); - } else { - result.push(...response.items.filter(filterItem)) - if (response.nextRange) { - await step(await client.contracts.get(contractsEndpoint, response.nextRange )); - } - } - } - let response = await client.contracts.get(contractsEndpoint) - await step(response) - return result; -} - -foldContracts(() => true).then(contracts => console.log(contracts.length)); - -let address = "addr_test1qz4y0hs2kwmlpvwc6xtyq6m27xcd3rx5v95vf89q24a57ux5hr7g3tkp68p0g099tpuf3kyd5g80wwtyhr8klrcgmhasu26qcn"; - -client.contracts.post( - contractsEndpoint, - { contract: "close" - , minUTxODeposit: 2000000 - , changeAddress: address - } -).then(function(response) { - console.log(response); - if(typeof response === "number") { - console.log("Error: ", response); - } else { - client.contract.get(response.endpoint).then(function(response) { - console.log(response); - }); - } -}); diff --git a/src/language/core/v1/examples/contract-one-notify.ts b/src/language/core/v1/examples/contract-one-notify.ts new file mode 100644 index 00000000..0dc05d84 --- /dev/null +++ b/src/language/core/v1/examples/contract-one-notify.ts @@ -0,0 +1,19 @@ +/* eslint-disable sort-keys-fix/sort-keys-fix */ + +import { Contract } from "../semantics/contract"; +import { close } from "../semantics/contract/close"; +import { Timeout } from "../semantics/contract/when"; + + +/** + * Marlowe Example : A contract with One Step (one true notify) + */ + +export const oneNotifyTrue : (notifyTimeout:Timeout) => Contract + = (notifyTimeout) => + ({ when :[{ case :{ notify_if: true } + , then : close}] + , timeout : notifyTimeout + , timeout_continuation : close}) + + diff --git a/src/language/core/v1/examples/index.ts b/src/language/core/v1/examples/index.ts new file mode 100644 index 00000000..1a28ebff --- /dev/null +++ b/src/language/core/v1/examples/index.ts @@ -0,0 +1,2 @@ +export * from './swap'; + diff --git a/src/language/core/v1/examples/swap.ts b/src/language/core/v1/examples/swap.ts new file mode 100644 index 00000000..2dc193e2 --- /dev/null +++ b/src/language/core/v1/examples/swap.ts @@ -0,0 +1,117 @@ +/* eslint-disable sort-keys-fix/sort-keys-fix */ + +import { Contract } from "../semantics/contract"; +import { close } from "../semantics/contract/close"; +import { Party, role } from "../semantics/contract/common/payee/party"; +import { ada, Token } from "../semantics/contract/common/token"; +import { Value, constant, mulValue } from "../semantics/contract/common/value"; +import { Timeout } from "../semantics/contract/when"; +import { InputDeposit } from "../semantics/contract/when/input/deposit"; + + +/** + * Marlowe Example : Swap + * Description : + * Takes Ada from one party and dollar tokens from another party, and it swaps them atomically. + */ + +export const swap : ( adaDepositTimeout:Timeout + , tokenDepositTimeout:Timeout + , amountOfADA:bigint + , amountOfToken:bigint + , token:Token) => Contract + = (adaDepositTimeout, tokenDepositTimeout,amountOfADA,amountOfToken,token) => + ({ when :[{ case :{ party: role('Ada provider') + , deposits: mulValue(constant(1_000_000n), amountOfADA) + , of_token: ada + , into_account: role('Ada provider') + } + , then : { when :[{ case :{ party: role('Token provider') + , deposits: amountOfToken + , of_token: token + , into_account: role('Token provider') + } + , then : { pay:mulValue(constant(1_000_000n), amountOfADA) + , token: ada + , from_account: role('Ada provider') + , to: {party : role('Token provider')} + , then: ({ pay:amountOfToken + , token: token + , from_account: role('Token provider') + , to: {party : role('Ada provider') } + , then: close})} + }] + , timeout : tokenDepositTimeout + , timeout_continuation : { pay: mulValue(constant(1_000_000n), amountOfADA) + , token: ada + , from_account: role('Ada provider') + , to: {party :role('Ada provider') } + , then: close}}}] + , timeout : adaDepositTimeout + , timeout_continuation : close}) + + +export type SwapWithExpectedInputs = + { swap : Contract + , adaProviderInputDeposit : InputDeposit + , tokenProviderInputDeposit : InputDeposit} + +export type SwapRequest + = { adaDepositTimeout:Timeout + , tokenDepositTimeout:Timeout + , amountOfADA:bigint + , amountOfToken:bigint + , token:Token } + +export const swapWithExpectedInputs + : (request :SwapRequest )=> SwapWithExpectedInputs + = (request) => + ({ swap : swap(request.adaDepositTimeout, request.tokenDepositTimeout,request.amountOfADA,request.amountOfToken,request.token) + , adaProviderInputDeposit : { input_from_party: role ('Ada provider') + , that_deposits: 1_000_000n * request.amountOfADA + , of_token: ada + , into_account: role('Ada provider') } + , tokenProviderInputDeposit : { input_from_party: role('Token provider') + , that_deposits: request.amountOfToken + , of_token: request.token + , into_account: role('Token provider') } + }) + + +// Slightly different version of where the providers need to withdraw themselves the token at the end. +export const swapWithRequiredWithdrawal : ( adaDepositTimeout:Timeout + , tokenDepositTimeout:Timeout + , amountOfADA:bigint + , amountOfToken:bigint + , token:Token) => Contract + = (adaDepositTimeout, tokenDepositTimeout,amountOfADA,amountOfToken,token) => + ({ when :[{ case :{ party: role('Ada provider') + , deposits: mulValue(constant(1_000_000n), amountOfADA) + , of_token: ada + , into_account: role('Ada provider') + } + , then : { when :[{ case :{ party: role('Token provider') + , deposits: amountOfToken + , of_token: token + , into_account: role('Token provider') + } + , then : close + }] + , timeout : tokenDepositTimeout + , timeout_continuation : close}}] + , timeout : adaDepositTimeout + , timeout_continuation : close}) + +export const swapWithRequiredWithdrawalAndExpectedInputs + : (request :SwapRequest )=> SwapWithExpectedInputs + = (request) => + ({ swap : swapWithRequiredWithdrawal(request.adaDepositTimeout, request.tokenDepositTimeout,request.amountOfADA,request.amountOfToken,request.token) + , adaProviderInputDeposit : { input_from_party: role ('Ada provider') + , that_deposits: 1_000_000n * request.amountOfADA + , of_token: ada + , into_account: role('Ada provider') } + , tokenProviderInputDeposit : { input_from_party: role('Token provider') + , that_deposits: request.amountOfToken + , of_token: request.token + , into_account: role('Token provider') } + }) diff --git a/src/language/core/v1/semantics/contract/assert.ts b/src/language/core/v1/semantics/contract/assert.ts new file mode 100644 index 00000000..c3c6bc53 --- /dev/null +++ b/src/language/core/v1/semantics/contract/assert.ts @@ -0,0 +1,12 @@ +import * as t from "io-ts"; +import { Contract } from "."; +import { Observation } from "./common/observations"; + +export type Assert + = { assert: Observation + , then: Contract } + +export const Assert : t.Type + = t.recursion('Assert', () => + t.type ({ assert: Observation + , then: Contract })) \ No newline at end of file diff --git a/src/language/core/v1/semantics/contract/close.ts b/src/language/core/v1/semantics/contract/close.ts new file mode 100644 index 00000000..69817ff1 --- /dev/null +++ b/src/language/core/v1/semantics/contract/close.ts @@ -0,0 +1,5 @@ +import * as t from "io-ts"; + +export const close = 'close' +export type Close = t.TypeOf +export const Close = t.literal('close') \ No newline at end of file diff --git a/src/language/core/v1/semantics/contract/common/address.ts b/src/language/core/v1/semantics/contract/common/address.ts new file mode 100644 index 00000000..37439c76 --- /dev/null +++ b/src/language/core/v1/semantics/contract/common/address.ts @@ -0,0 +1,7 @@ + +import * as t from "io-ts" + +export type AddressBech32 = string +export const AddressBech32 = t.string + + diff --git a/src/language/core/v1/semantics/contract/common/observations.ts b/src/language/core/v1/semantics/contract/common/observations.ts new file mode 100644 index 00000000..df980ee6 --- /dev/null +++ b/src/language/core/v1/semantics/contract/common/observations.ts @@ -0,0 +1,75 @@ +import * as t from "io-ts"; +import { iso, Newtype } from "newtype-ts"; +import { fromNewtype } from "io-ts-types"; +import { ChoiceId, Value } from "./value"; + +export type And = { both: Observation, and: Observation } +export const And : t.Type = t.recursion('And', () => t.type({ both: Observation, and: Observation })) + +export type Or = { either: Observation, or: Observation } +export const Or : t.Type = t.recursion('Or', () => t.type({ either: Observation, or: Observation })) + +export type Not = { not: Observation } +export const Not : t.Type = t.recursion('Not', () => t.type({ not: Observation })) + +export type Chose = { chose_something_for: ChoiceId } +export const Chose : t.Type = t.recursion('Chose', () => t.type({ chose_something_for: ChoiceId })) + +export type Equal + = { value: Value + , equal_to: Value } + +export const Equal : t.Type = t.recursion('Equal', () => + t.type({ value: Value, equal_to: Value })) + +export type Greater + = { value: Value + , gt: Value } + +export const Greater : t.Type = t.recursion('Greater', () => + t.type({ value: Value, gt: Value })) + +export type GreaterOrEqual += { value: Value + , ge_than: Value } + +export const GreaterOrEqual : t.Type = t.recursion('GreaterOrEqual', () => + t.type({ value: Value, ge_than: Value })) + +export type Lower += { value: Value + , lt: Value } + +export const Lower : t.Type = t.recursion('Lower', () => + t.type({ value: Value, lt: Value })) + +export type LowerOrEqual + = { value: Value + , le_than: Value } + +export const LowerOrEqual : t.Type = t.recursion('LowerOrEqual', () => + t.type({ value: Value, le_than: Value })) + +export type Observation = + | And + | Or + | Not + | Chose + | Equal + | Greater + | GreaterOrEqual + | Lower + | LowerOrEqual + | boolean + +export const Observation : t.Type = t.recursion('Observation', () => + t.union([ And + , Or + , Not + , Chose + , Equal + , Greater + , GreaterOrEqual + , Lower + , LowerOrEqual + , t.boolean])) \ No newline at end of file diff --git a/src/language/core/v1/semantics/contract/common/payee/account.ts b/src/language/core/v1/semantics/contract/common/payee/account.ts new file mode 100644 index 00000000..58b61e56 --- /dev/null +++ b/src/language/core/v1/semantics/contract/common/payee/account.ts @@ -0,0 +1,12 @@ +import * as t from "io-ts"; +import { Party } from "./party"; +import { Token } from "../token"; + +export type AccountId = t.TypeOf +export const AccountId = Party + +export type Account = t.TypeOf +export const Account = t.tuple([t.tuple([AccountId,Token]),t.bigint]) + +export type Accounts = t.TypeOf +export const Accounts = t.array(Account) \ No newline at end of file diff --git a/src/language/core/v1/semantics/contract/common/payee/index..ts b/src/language/core/v1/semantics/contract/common/payee/index..ts new file mode 100644 index 00000000..d64dd7de --- /dev/null +++ b/src/language/core/v1/semantics/contract/common/payee/index..ts @@ -0,0 +1,9 @@ +import * as t from "io-ts"; +import { AccountId } from "./account"; +import { Party } from "./party"; + + +export type Payee = t.TypeOf +export const Payee = t.union([ t.type({ account: AccountId }) + , t.type({ party: Party }) + ]) \ No newline at end of file diff --git a/src/language/core/v1/semantics/contract/common/payee/party.ts b/src/language/core/v1/semantics/contract/common/payee/party.ts new file mode 100644 index 00000000..36f279e4 --- /dev/null +++ b/src/language/core/v1/semantics/contract/common/payee/party.ts @@ -0,0 +1,20 @@ +import * as t from "io-ts"; +import { iso, Newtype } from "newtype-ts"; +import { fromNewtype } from "io-ts-types"; +import { TokenName } from "../token"; +import { AddressBech32 } from "../address"; + + +export type Address = t.TypeOf +export const Address = t.type({address:AddressBech32}) + + +export const role = (roleToken:TokenName) => ({ role_token: roleToken }) +export type Role = t.TypeOf +export const Role = t.type({role_token: TokenName }) + +export const party = (party:Role|Address) => party +export type Party = t.TypeOf +export const Party = t.union([Address,Role]) + + \ No newline at end of file diff --git a/src/language/core/v1/semantics/contract/common/policyId.ts b/src/language/core/v1/semantics/contract/common/policyId.ts new file mode 100644 index 00000000..cea5d7ae --- /dev/null +++ b/src/language/core/v1/semantics/contract/common/policyId.ts @@ -0,0 +1,4 @@ +import * as t from "io-ts" + +export type PolicyId = string +export const PolicyId = t.string \ No newline at end of file diff --git a/src/language/core/v1/semantics/contract/common/token.ts b/src/language/core/v1/semantics/contract/common/token.ts new file mode 100644 index 00000000..4589c23e --- /dev/null +++ b/src/language/core/v1/semantics/contract/common/token.ts @@ -0,0 +1,12 @@ +import * as t from "io-ts" +import { PolicyId } from "./policyId" + + +export type TokenName = t.TypeOf +export const TokenName = t.string + +export type Token = t.TypeOf +export const Token = t.type({currency_symbol:PolicyId,token_name:TokenName}) + +export const token = (currency_symbol :PolicyId,token_name: TokenName) => ({ currency_symbol: currency_symbol, token_name: token_name }) +export const ada: Token = token('','') \ No newline at end of file diff --git a/src/language/core/v1/semantics/contract/common/value.ts b/src/language/core/v1/semantics/contract/common/value.ts new file mode 100644 index 00000000..e0af5575 --- /dev/null +++ b/src/language/core/v1/semantics/contract/common/value.ts @@ -0,0 +1,91 @@ +import * as t from "io-ts"; +import { iso, Newtype } from "newtype-ts"; +import { fromNewtype } from "io-ts-types"; +import { Party } from "../common/payee/party"; +import { Observation } from "./observations"; + +export const constant = (constant:bigint) => constant +export type Constant = t.TypeOf +export const Constant = t.bigint + +export type TimeIntervalStart = t.TypeOf +export const TimeIntervalStart = t.literal('time_interval_start') + +export type TimeIntervalEnd = t.TypeOf +export const TimeIntervalEnd = t.literal('time_interval_end') + +export type NegValue = { negate: Value } +export const NegValue : t.Type = t.recursion('NegValue', () => t.type({ negate: Value })) + +export type AddValue + = { add: Value + , and: Value } + +export const AddValue : t.Type = t.recursion('AddValue', () => t.type({ add: Value, and: Value })) + +export type SubValue + = { value: Value + , minus: Value } + +export const SubValue : t.Type = t.recursion('SubValue', () => t.type({ value: Value, minus: Value })) + +export const mulValue = (multiply:Value,times:Value) => ({ multiply: multiply, times: times }) +export type MulValue + = { multiply: Value + , times: Value } + +export const MulValue : t.Type = t.recursion('MulValue', () => t.type({ multiply: Value, times: Value })) + +export type DivValue + = { divide: Value + , by: Value } + +export const DivValue : t.Type = t.recursion('DivValue', () => t.type({ divide: Value, by: Value })) + +export type ChoiceName = t.TypeOf +export const ChoiceName = t.string + +export type ChoiceId = + { choice_name: ChoiceName + , choice_owner: Party } + +export const ChoiceId : t.Type = t.recursion('ChoiceId', () => t.type({ choice_name: ChoiceName, choice_owner: Party })) + +export type ChoiceValue = { value_of_choice: ChoiceId } +export const ChoiceValue : t.Type = t.recursion('ChoiceValue', () => t.type({ value_of_choice: ChoiceId })) + + +export type ValueId = t.TypeOf +export const ValueId = t.string + +export type UseValue = { use_value: ValueId } +export const UseValue : t.Type = t.recursion('UseValue', () => t.type({ use_value: ValueId })) + +export type Cond = { if: Observation, then: Value, else: Value } +export const Cond : t.Type = t.recursion('Cond', () => t.type({ if: Observation, then: Value, else: Value })) + +export type Value = + | Constant + | NegValue + | AddValue + | SubValue + | MulValue + | DivValue + | ChoiceValue + | TimeIntervalStart + | TimeIntervalEnd + | UseValue + | Cond + +export const Value :t.Type = t.recursion('Value', () => + t.union([ Constant + , NegValue + , AddValue + , SubValue + , MulValue + , DivValue + , ChoiceValue + , TimeIntervalStart + , TimeIntervalEnd + , UseValue + , Cond])) \ No newline at end of file diff --git a/src/language/core/v1/semantics/contract/if.ts b/src/language/core/v1/semantics/contract/if.ts new file mode 100644 index 00000000..ad07aa19 --- /dev/null +++ b/src/language/core/v1/semantics/contract/if.ts @@ -0,0 +1,14 @@ +import * as t from "io-ts"; +import { Contract } from "."; +import { Observation } from "./common/observations"; + +export type If + = { if: Observation + , then: Contract + , else: Contract } + +export const If : t.Type + = t.recursion('If', () => + t.type ({ if: Observation + , then: Contract + , else: Contract })) \ No newline at end of file diff --git a/src/language/core/v1/semantics/contract/index.ts b/src/language/core/v1/semantics/contract/index.ts new file mode 100644 index 00000000..66662c1b --- /dev/null +++ b/src/language/core/v1/semantics/contract/index.ts @@ -0,0 +1,28 @@ + +import * as t from "io-ts"; + +import { Assert } from "./assert"; +import { Close } from "./close"; +import { If } from "./if"; +import { Let } from "./let"; +import { Pay } from "./pay"; +import { When } from "./when"; + + +export type Contract = + | Close + | Pay + | If + | When + | Let + | Assert + +export const Contract : t.Type + = t.recursion('Contract', () => + t.union ([ Close + , Pay + , If + , When + , Let + , Assert])) + diff --git a/src/language/core/v1/semantics/contract/let.ts b/src/language/core/v1/semantics/contract/let.ts new file mode 100644 index 00000000..ee42aa7a --- /dev/null +++ b/src/language/core/v1/semantics/contract/let.ts @@ -0,0 +1,14 @@ +import * as t from "io-ts"; +import { Contract } from "."; +import { ValueId, Value } from "./common/value"; + +export type Let + = { let: ValueId + , be: Value + , then: Contract } + +export const Let : t.Type + = t.recursion('Let', () => + t.type ({ let: ValueId + , be: Value + , then: Contract })) \ No newline at end of file diff --git a/src/language/core/v1/semantics/contract/pay.ts b/src/language/core/v1/semantics/contract/pay.ts new file mode 100644 index 00000000..970d66da --- /dev/null +++ b/src/language/core/v1/semantics/contract/pay.ts @@ -0,0 +1,31 @@ +import * as t from "io-ts" +import { AccountId } from "./common/payee/account"; +import { Contract } from "."; +import { Payee } from "./common/payee/index."; +import { Token } from "./common/token"; +import { Value } from "./common/value"; + +export const pay = ( + pay: Value + , token: Token + , from_account: AccountId + , to: Payee + , then: Contract) => ({pay:pay, token: token, from_account: from_account, to: to, then: then}) + +export type Pay = { + pay: Value; + token: Token; + from_account: AccountId; + to: Payee; + then: Contract; + } + +export const Pay += t.recursion('Pay', () => + t.type({ pay: Value + , token: Token + , from_account: AccountId + , to: Payee + , then: Contract + })) + \ No newline at end of file diff --git a/src/language/core/v1/semantics/contract/when/action/choice.ts b/src/language/core/v1/semantics/contract/when/action/choice.ts new file mode 100644 index 00000000..71e240f5 --- /dev/null +++ b/src/language/core/v1/semantics/contract/when/action/choice.ts @@ -0,0 +1,20 @@ +import * as t from "io-ts"; +import { ChoiceId } from "../../common/value"; + +export type Bound + = { from: bigint + , to: bigint } + +export const Bound : t.Type + = t.recursion('Bound', () => + t.type ({ from: t.bigint + , to: t.bigint })) + +export type Choice = + { choose_between: Bound[] + , for_choice: ChoiceId } + +export const Choice : t.Type + = t.recursion('Choice', () => + t.type ({ choose_between: t.array(Bound) + , for_choice: ChoiceId })) \ No newline at end of file diff --git a/src/language/core/v1/semantics/contract/when/action/deposit.ts b/src/language/core/v1/semantics/contract/when/action/deposit.ts new file mode 100644 index 00000000..dd9d7ae8 --- /dev/null +++ b/src/language/core/v1/semantics/contract/when/action/deposit.ts @@ -0,0 +1,18 @@ +import * as t from "io-ts"; +import { AccountId } from "../../common/payee/account"; +import { Party } from "../../common/payee/party"; +import { Token } from "../../common/token"; +import { Value } from "../../common/value"; + +export type Deposit = + | { party: Party + , deposits: Value + , of_token: Token + , into_account: AccountId } + +export const Deposit : t.Type + = t.recursion('Deposit', () => + t.type ({ party: Party + , deposits: Value + , of_token: Token + , into_account: AccountId })) \ No newline at end of file diff --git a/src/language/core/v1/semantics/contract/when/action/index.ts b/src/language/core/v1/semantics/contract/when/action/index.ts new file mode 100644 index 00000000..bb382085 --- /dev/null +++ b/src/language/core/v1/semantics/contract/when/action/index.ts @@ -0,0 +1,22 @@ +import * as t from "io-ts"; + +import { Choice } from "./choice"; +import { Deposit } from "./deposit"; +import { Notify } from "./notify"; + +export type Action = + | Deposit + | Choice + | Notify + +export const Action : t.Type + = t.recursion('Action', () => + t.union ([ Deposit + , Choice + , Notify])) + + + + + + diff --git a/src/language/core/v1/semantics/contract/when/action/notify.ts b/src/language/core/v1/semantics/contract/when/action/notify.ts new file mode 100644 index 00000000..6516f12a --- /dev/null +++ b/src/language/core/v1/semantics/contract/when/action/notify.ts @@ -0,0 +1,8 @@ +import * as t from "io-ts"; +import { Observation } from "../../common/observations"; + +export type Notify = { notify_if: Observation } + +export const Notify : t.Type + = t.recursion('Notify', () => + t.type ({ notify_if: Observation })) diff --git a/src/language/core/v1/semantics/contract/when/index.ts b/src/language/core/v1/semantics/contract/when/index.ts new file mode 100644 index 00000000..4a307677 --- /dev/null +++ b/src/language/core/v1/semantics/contract/when/index.ts @@ -0,0 +1,31 @@ +import * as t from "io-ts"; +import { Action } from "./action"; +import { Contract } from ".."; +import { pipe } from "fp-ts/lib/function"; +import getUnixTime from 'date-fns/getUnixTime' + + +export type When + = { when: Case[] + , timeout: Timeout + , timeout_continuation: Contract } + +export const When : t.Type + = t.recursion('When', () => + t.type ({ when: t.array(Case) + , timeout: Timeout + , timeout_continuation: Contract })) + +export type Case + = { case: Action + , then: Contract } + +export const Case : t.Type + = t.recursion('Case', () => + t.type ({ case: Action + , then: Contract })) + +export type Timeout = t.TypeOf +export const Timeout = t.bigint + +export const datetoTimeout = (date:Date):Timeout => pipe(date,getUnixTime,(a) => a * 1_000,BigInt) \ No newline at end of file diff --git a/src/language/core/v1/semantics/contract/when/input/choice.ts b/src/language/core/v1/semantics/contract/when/input/choice.ts new file mode 100644 index 00000000..bdc1fd99 --- /dev/null +++ b/src/language/core/v1/semantics/contract/when/input/choice.ts @@ -0,0 +1,10 @@ +import * as t from "io-ts"; +import { ChoiceId } from "../../common/value"; + +export type ChosenNum = t.TypeOf +export const ChosenNum = t.bigint + +export type InputChoice = t.TypeOf +export const InputChoice + = t.type ({ for_choice_id: ChoiceId + , input_that_chooses_num: ChosenNum }) \ No newline at end of file diff --git a/src/language/core/v1/semantics/contract/when/input/deposit.ts b/src/language/core/v1/semantics/contract/when/input/deposit.ts new file mode 100644 index 00000000..c3f52cc9 --- /dev/null +++ b/src/language/core/v1/semantics/contract/when/input/deposit.ts @@ -0,0 +1,11 @@ +import * as t from "io-ts"; +import { AccountId } from "../../common/payee/account"; +import { Party } from "../../common/payee/party"; +import { Token } from "../../common/token"; + +export type InputDeposit = t.TypeOf +export const InputDeposit + = t.type ({ input_from_party: Party + , that_deposits: t.bigint + , of_token: Token + , into_account: AccountId }) \ No newline at end of file diff --git a/src/language/core/v1/semantics/contract/when/input/index.ts b/src/language/core/v1/semantics/contract/when/input/index.ts new file mode 100644 index 00000000..8e8e45b2 --- /dev/null +++ b/src/language/core/v1/semantics/contract/when/input/index.ts @@ -0,0 +1,30 @@ +import * as t from "io-ts"; +import { Contract } from "../.."; + +import { InputChoice } from "./choice"; +import { InputDeposit } from "./deposit"; +import { InputNotify } from "./notify"; + +export type BuiltinByteString = t.TypeOf +export const BuiltinByteString = t.string + +export type InputContent = t.TypeOf +export const InputContent + = t.union ([ InputDeposit + , InputChoice + , InputNotify]) + +export type NormalInput = t.TypeOf +export const NormalInput = InputContent + +export type MerkleizedInput = t.TypeOf +export const MerkleizedInput + = t.intersection( + [ InputContent + ,t.partial({ continuation_hash : BuiltinByteString + , merkleized_continuation:Contract }) + ]) + + +export type Input = t.TypeOf +export const Input = t.union([NormalInput,MerkleizedInput]) \ No newline at end of file diff --git a/src/language/core/v1/semantics/contract/when/input/notify.ts b/src/language/core/v1/semantics/contract/when/input/notify.ts new file mode 100644 index 00000000..06ff9621 --- /dev/null +++ b/src/language/core/v1/semantics/contract/when/input/notify.ts @@ -0,0 +1,6 @@ +import * as t from "io-ts"; + + +export const inputNotify = 'input_notify' +export type InputNotify = t.TypeOf +export const InputNotify = t.literal ('input_notify') diff --git a/src/language/index.ts b/src/language/index.ts new file mode 100644 index 00000000..bd16428c --- /dev/null +++ b/src/language/index.ts @@ -0,0 +1 @@ +export * from './legacy/dsl'; diff --git a/src/language/legacy/dsl.ts b/src/language/legacy/dsl.ts new file mode 100644 index 00000000..f03ecf7f --- /dev/null +++ b/src/language/legacy/dsl.ts @@ -0,0 +1,288 @@ +/* eslint-disable no-use-before-define */ +/* eslint-disable sort-keys-fix/sort-keys-fix */ + +export type Party = { address: string } | { role_token: string }; + +export type SomeNumber = number | string | bigint; + +export const coerceNumber = function (n: SomeNumber): BigInt { + const isNumeric = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i; + if (typeof n === 'string' && isNumeric.test(String(n))) { + return BigInt(n); + } else if (typeof n === 'bigint') { + return BigInt(n); + } else if (typeof n === 'number') { + return BigInt(n); + } + throw new Error('Not a valid number'); +}; + +export const Address = function (address: string): Party { + return { address }; +}; + +export const Role = function (roleToken: string): Party { + return { role_token: roleToken }; +}; + +export type AccountId = Party; + +export type ChoiceId = { choice_name: string; choice_owner: Party }; + +export const ChoiceId = function (choiceName: string, choiceOwner: Party): ChoiceId { + return { choice_name: choiceName, choice_owner: choiceOwner }; +}; + +export type Token = { currency_symbol: string; token_name: string }; + +export const Token = function (currencySymbol: string, tokenName: string): Token { + const isBase16 = /^([\da-f]{2})*$/g; + if (isBase16.test(currencySymbol)) { + return { currency_symbol: currencySymbol, token_name: tokenName }; + } + throw new Error('Currency symbol must be base16'); +}; + +export const ada: Token = { currency_symbol: '', token_name: '' }; + +export type ValueId = string; + +export const ValueId = function (valueIdentifier: string): ValueId { + return valueIdentifier; +}; + +export type Value = + | { amount_of_token: Token; in_account: AccountId } + | BigInt + | { constant_param: String } + | { negate: Value } + | { add: Value; and: Value } + | { value: Value; minus: Value } + | { multiply: Value; times: Value } + | { divide: Value; by: Value } + | { value_of_choice: ChoiceId } + | 'time_interval_start' + | 'time_interval_end' + | { use_value: ValueId } + | { if: Observation; then: Value; else: Value }; + +export type EValue = SomeNumber | Value; + +const coerceValue = function (val: EValue): Value { + if (typeof val === 'number') { + if (val > Number.MAX_SAFE_INTEGER || val < -Number.MAX_SAFE_INTEGER) { + throw new Error('Unsafe use of JavaScript numbers. For amounts this large, please use BigNumber.'); + } + return BigInt(val); + } else if (typeof val === 'bigint') { + return BigInt(val); + } else if (typeof val === 'string' && val !== 'time_interval_start' && val !== 'time_interval_end') { + return BigInt(val); + } + return val; +}; + +export const AvailableMoney = function (token: Token, accountId: AccountId): Value { + return { amount_of_token: token, in_account: accountId }; +}; + +export const Constant = function (number: SomeNumber): Value { + return coerceNumber(number); +}; + +export const ConstantParam = function (paramName: String): Value { + return { constant_param: paramName }; +}; + +export const NegValue = function (value: EValue): Value { + return { negate: coerceValue(value) }; +}; + +export const AddValue = function (lhs: EValue, rhs: EValue): Value { + return { add: coerceValue(lhs), and: coerceValue(rhs) }; +}; + +export const SubValue = function (lhs: EValue, rhs: EValue): Value { + return { value: coerceValue(lhs), minus: coerceValue(rhs) }; +}; + +export const MulValue = function (lhs: EValue, rhs: EValue): Value { + return { multiply: coerceValue(lhs), times: coerceValue(rhs) }; +}; + +export const DivValue = function (lhs: EValue, rhs: EValue): Value { + return { divide: coerceValue(lhs), by: coerceValue(rhs) }; +}; + +export const ChoiceValue = function (choiceId: ChoiceId): Value { + return { value_of_choice: choiceId }; +}; + +export const TimeIntervalStart: Value = 'time_interval_start'; + +export const TimeIntervalEnd: Value = 'time_interval_end'; + +export const UseValue = function (valueId: ValueId): Value { + return { use_value: valueId }; +}; + +export const Cond = function (obs: Observation, contThen: EValue, contElse: EValue): Value { + return { if: obs, then: coerceValue(contThen), else: coerceValue(contElse) }; +}; + +export type Observation = + | { both: Observation; and: Observation } + | { either: Observation; or: Observation } + | { not: Observation } + | { chose_something_for: ChoiceId } + | { value: Value; ge_than: Value } + | { value: Value; gt: Value } + | { value: Value; lt: Value } + | { value: Value; le_than: Value } + | { value: Value; equal_to: Value } + | boolean; + +export const AndObs = function (lhs: Observation, rhs: Observation): Observation { + return { both: lhs, and: rhs }; +}; + +export const OrObs = function (lhs: Observation, rhs: Observation): Observation { + return { either: lhs, or: rhs }; +}; + +export const NotObs = function (obs: Observation): Observation { + return { not: obs }; +}; + +export const ChoseSomething = function (choiceId: ChoiceId): Observation { + return { chose_something_for: choiceId }; +}; + +export const ValueGE = function (lhs: EValue, rhs: EValue): Observation { + return { value: coerceValue(lhs), ge_than: coerceValue(rhs) }; +}; + +export const ValueGT = function (lhs: EValue, rhs: EValue): Observation { + return { value: coerceValue(lhs), gt: coerceValue(rhs) }; +}; + +export const ValueLT = function (lhs: EValue, rhs: EValue): Observation { + return { value: coerceValue(lhs), lt: coerceValue(rhs) }; +}; + +export const ValueLE = function (lhs: EValue, rhs: EValue): Observation { + return { value: coerceValue(lhs), le_than: coerceValue(rhs) }; +}; + +export const ValueEQ = function (lhs: EValue, rhs: EValue): Observation { + return { value: coerceValue(lhs), equal_to: coerceValue(rhs) }; +}; + +export const TrueObs: Observation = true; + +export const FalseObs: Observation = false; + +export type Bound = { from: BigInt; to: BigInt }; + +export const Bound = function (boundMin: SomeNumber, boundMax: SomeNumber): Bound { + return { from: coerceNumber(boundMin), to: coerceNumber(boundMax) }; +}; + +export type Action = + | { party: Party; deposits: Value; of_token: Token; into_account: AccountId } + | { choose_between: Bound[]; for_choice: ChoiceId } + | { notify_if: Observation }; + +export const Deposit = function (accId: AccountId, party: Party, token: Token, value: EValue): Action { + return { + deposits: coerceValue(value), + into_account: accId, + of_token: token, + party + }; +}; + +export const Choice = function (choiceId: ChoiceId, bounds: Bound[]): Action { + return { choose_between: bounds, for_choice: choiceId }; +}; + +export const Notify = function (obs: Observation): Action { + return { notify_if: obs }; +}; + +export type Payee = { account: AccountId } | { party: Party }; + +export const Account = function (party: Party): Payee { + return { account: party }; +}; + +export const Party = function (party: Party): Payee { + return { party }; +}; + +export type Case = { case: Action; then: Contract }; + +export const Case = function (caseAction: Action, continuation: Contract): Case { + return { case: caseAction, then: continuation }; +}; + +export type Timeout = { time_param: String } | BigInt; + +export type ETimeout = SomeNumber | Timeout; + +export const TimeParam = function (paramName: String): Timeout { + return { time_param: paramName }; +}; + +export type Contract = + | 'close' + | { + pay: Value; + token: Token; + from_account: AccountId; + to: Payee; + then: Contract; + } + | { if: Observation; then: Contract; else: Contract } + | { when: Case[]; timeout: Timeout; timeout_continuation: Contract } + | { let: ValueId; be: Value; then: Contract } + | { assert: Observation; then: Contract }; + +export const Close: Contract = 'close'; + +export const Pay = function ( + accId: AccountId, + payee: Payee, + token: Token, + value: EValue, + continuation: Contract +): Contract { + return { + pay: coerceValue(value), + token, + from_account: accId, + to: payee, + then: continuation + }; +}; + +export const If = function (obs: Observation, contThen: Contract, contElse: Contract): Contract { + return { if: obs, then: contThen, else: contElse }; +}; + +export const When = function (cases: Case[], timeout: ETimeout, timeoutCont: Contract): Contract { + const coercedTimeout: Timeout = typeof timeout === 'object' ? timeout : coerceNumber(timeout); + return { + when: cases, + timeout: coercedTimeout, + timeout_continuation: timeoutCont + }; +}; + +export const Let = function (valueId: ValueId, value: Value, cont: Contract): Contract { + return { let: valueId, be: value, then: cont }; +}; + +export const Assert = function (obs: Observation, cont: Contract): Contract { + return { assert: obs, then: cont }; +}; diff --git a/src/language/legacy/examples/contractForDifferences.ts b/src/language/legacy/examples/contractForDifferences.ts new file mode 100644 index 00000000..83404b9c --- /dev/null +++ b/src/language/legacy/examples/contractForDifferences.ts @@ -0,0 +1,217 @@ +/* eslint-disable sort-keys-fix/sort-keys-fix */ +import { + Account, + AddValue, + Bound, + Case, + Choice, + ChoiceId, + ChoiceValue, + Close, + Cond, + Constant, + ConstantParam, + Contract, + Deposit, + ETimeout, + If, + Let, + Party, + Pay, + Role, + SubValue, + TimeParam, + UseValue, + Value, + ValueGT, + ValueId, + ValueLT, + When, + ada +} from '../dsl' + +/** + * Marlowe Example : Contract For Differences + * Description : + * "Party" and "Counterparty" deposit 100 Ada and after 60 slots is redistributed + * depending on the change in a given trade price reported by "Oracle". + * If the price increases, the difference goes to "Counterparty"; + * if it decreases, the difference goes to "Party", up to a maximum of 100 Ada. + */ + +/* We can set explicitRefunds true to run Close refund analysis + but we get a shorter contract if we set it to false */ +const explicitRefunds: Boolean = false; + +const party: Party = Role('Party'); +const counterparty: Party = Role('Counterparty'); +const oracle: Party = Role('Oracle'); + +const partyDeposit: Value = ConstantParam('Amount paid by party'); +const counterpartyDeposit: Value = ConstantParam('Amount paid by counterparty'); +const bothDeposits: Value = AddValue(partyDeposit, counterpartyDeposit); + +const priceBeginning: ChoiceId = ChoiceId('Price in first window', oracle); +const priceEnd: ChoiceId = ChoiceId('Price in second window', oracle); + +const decreaseInPrice: ValueId = 'Decrease in price'; +const increaseInPrice: ValueId = 'Increase in price'; + +const initialDeposit = function ( + by: Party, + deposit: Value, + timeout: ETimeout, + timeoutContinuation: Contract, + continuation: Contract +): Contract { + return When([Case(Deposit(by, by, ada, deposit), continuation)], timeout, timeoutContinuation); +}; + +const oracleInput = function ( + choiceId: ChoiceId, + timeout: ETimeout, + timeoutContinuation: Contract, + continuation: Contract +): Contract { + return When([Case(Choice(choiceId, [Bound(0n, 1_000_000_000n)]), continuation)], timeout, timeoutContinuation); +}; + +const wait = function (timeout: ETimeout, continuation: Contract): Contract { + return When([], timeout, continuation); +}; + +const gtLtEq = function ( + value1: Value, + value2: Value, + gtContinuation: Contract, + ltContinuation: Contract, + eqContinuation: Contract +): Contract { + return If(ValueGT(value1, value2), gtContinuation, If(ValueLT(value1, value2), ltContinuation, eqContinuation)); +}; + +const recordDifference = function ( + name: ValueId, + choiceId1: ChoiceId, + choiceId2: ChoiceId, + continuation: Contract +): Contract { + return Let(name, SubValue(ChoiceValue(choiceId1), ChoiceValue(choiceId2)), continuation); +}; + +const transferUpToDeposit = function ( + from: Party, + payerDeposit: Value, + to: Party, + amount: Value, + continuation: Contract +): Contract { + return Pay(from, Account(to), ada, Cond(ValueLT(amount, payerDeposit), amount, payerDeposit), continuation); +}; + +const refund = function (who: Party, amount: Value, continuation: Contract): Contract { + if (explicitRefunds) { + return Pay(who, Party(who), ada, amount, continuation); + } + return continuation; +}; + +const refundBoth: Contract = refund(party, partyDeposit, refund(counterparty, counterpartyDeposit, Close)); + +const refundIfGtZero = function (who: Party, amount: Value, continuation: Contract): Contract { + if (explicitRefunds) { + return If(ValueGT(amount, Constant(0n)), refund(who, amount, continuation), continuation); + } + return continuation; +}; + +const refundUpToBothDeposits = function (who: Party, amount: Value, continuation: Contract): Contract { + if (explicitRefunds) { + return refund(who, Cond(ValueGT(amount, bothDeposits), bothDeposits, amount), continuation); + } + return continuation; +}; + +const refundAfterDifference = function ( + payer: Party, + payerDeposit: Value, + payee: Party, + payeeDeposit: Value, + difference: Value +): Contract { + return refundIfGtZero( + payer, + SubValue(payerDeposit, difference), + refundUpToBothDeposits(payee, AddValue(payeeDeposit, difference), Close) + ); +}; + +export const contractForDifferences: Contract = initialDeposit( + party, + partyDeposit, + TimeParam('Party deposit deadline'), + Close, + initialDeposit( + counterparty, + counterpartyDeposit, + TimeParam('Counterparty deposit deadline'), + refund(party, partyDeposit, Close), + wait( + TimeParam('First window beginning'), + oracleInput( + priceBeginning, + TimeParam('First window deadline'), + refundBoth, + wait( + TimeParam('Second window beginning'), + oracleInput( + priceEnd, + TimeParam('Second window deadline'), + refundBoth, + gtLtEq( + ChoiceValue(priceBeginning), + ChoiceValue(priceEnd), + recordDifference( + decreaseInPrice, + priceBeginning, + priceEnd, + transferUpToDeposit( + counterparty, + counterpartyDeposit, + party, + UseValue(decreaseInPrice), + refundAfterDifference( + counterparty, + counterpartyDeposit, + party, + partyDeposit, + UseValue(decreaseInPrice) + ) + ) + ), + recordDifference( + increaseInPrice, + priceEnd, + priceBeginning, + transferUpToDeposit( + party, + partyDeposit, + counterparty, + UseValue(increaseInPrice), + refundAfterDifference( + party, + partyDeposit, + counterparty, + counterpartyDeposit, + UseValue(increaseInPrice) + ) + ) + ), + refundBoth + ) + ) + ) + ) + ) + ) +); diff --git a/src/language/legacy/examples/contractForDifferencesWithOracle.ts b/src/language/legacy/examples/contractForDifferencesWithOracle.ts new file mode 100644 index 00000000..774615e7 --- /dev/null +++ b/src/language/legacy/examples/contractForDifferencesWithOracle.ts @@ -0,0 +1,235 @@ +/* eslint-disable sort-keys-fix/sort-keys-fix */ +import { + Account, + AddValue, + Bound, + Case, + Choice, + ChoiceId, + ChoiceValue, + Close, + Cond, + Constant, + ConstantParam, + Contract, + Deposit, + DivValue, + ETimeout, + If, + Let, + MulValue, + Party, + Pay, + Role, + SubValue, + TimeParam, + UseValue, + Value, + ValueGT, + ValueId, + ValueLT, + When, + ada +} from '../dsl' + +/** + * Marlowe Example : Contract For Differences with Oracle + * Description : + * "Party" and "Counterparty" deposit 100 Ada and after 60 slots these assets + * are redistributed depending on the change in price of 100 Ada worth of dollars + * between the start and the end of the contract. If the price increases, the difference + * goes to "Counterparty"; if it decreases, the difference goes to "Party", up to a maximum of 100 Ada. + */ +/* We can set explicitRefunds true to run Close refund analysis +but we get a shorter contract if we set it to false */ +const explicitRefunds: Boolean = false; + +const party: Party = Role('Party'); +const counterparty: Party = Role('Counterparty'); +const oracle: Party = Role('kraken'); + +const partyDeposit: Value = ConstantParam('Amount paid by party'); +const counterpartyDeposit: Value = ConstantParam('Amount paid by counterparty'); +const bothDeposits: Value = AddValue(partyDeposit, counterpartyDeposit); + +const priceBeginning: Value = ConstantParam('Amount of Ada to use as asset'); +const priceEnd: ValueId = ValueId('Price in second window'); + +const exchangeBeginning: ChoiceId = ChoiceId('dir-adausd', oracle); +const exchangeEnd: ChoiceId = ChoiceId('inv-adausd', oracle); + +const decreaseInPrice: ValueId = 'Decrease in price'; +const increaseInPrice: ValueId = 'Increase in price'; + +const initialDeposit = function ( + by: Party, + deposit: Value, + timeout: ETimeout, + timeoutContinuation: Contract, + continuation: Contract +): Contract { + return When([Case(Deposit(by, by, ada, deposit), continuation)], timeout, timeoutContinuation); +}; + +const oracleInput = function ( + choiceId: ChoiceId, + timeout: ETimeout, + timeoutContinuation: Contract, + continuation: Contract +): Contract { + return When([Case(Choice(choiceId, [Bound(0n, 100_000_000_000n)]), continuation)], timeout, timeoutContinuation); +}; + +const wait = function (timeout: ETimeout, continuation: Contract): Contract { + return When([], timeout, continuation); +}; + +const gtLtEq = function ( + value1: Value, + value2: Value, + gtContinuation: Contract, + ltContinuation: Contract, + eqContinuation: Contract +): Contract { + return If(ValueGT(value1, value2), gtContinuation, If(ValueLT(value1, value2), ltContinuation, eqContinuation)); +}; + +const recordEndPrice = function ( + name: ValueId, + choiceId1: ChoiceId, + choiceId2: ChoiceId, + continuation: Contract +): Contract { + const valueId = name; + const value = DivValue( + MulValue(priceBeginning, MulValue(ChoiceValue(choiceId1), ChoiceValue(choiceId2))), + Constant(10_000_000_000_000_000n) + ); + return Let(valueId, value, continuation); +}; + +const recordDifference = function (name: ValueId, val1: Value, val2: Value, continuation: Contract): Contract { + return Let(name, SubValue(val1, val2), continuation); +}; + +const transferUpToDeposit = function ( + from: Party, + payerDeposit: Value, + to: Party, + amount: Value, + continuation: Contract +): Contract { + return Pay(from, Account(to), ada, Cond(ValueLT(amount, payerDeposit), amount, payerDeposit), continuation); +}; + +const refund = function (who: Party, amount: Value, continuation: Contract): Contract { + if (explicitRefunds) { + return Pay(who, Party(who), ada, amount, continuation); + } + return continuation; +}; + +const refundBoth: Contract = refund(party, partyDeposit, refund(counterparty, counterpartyDeposit, Close)); + +const refundIfGtZero = function (who: Party, amount: Value, continuation: Contract): Contract { + if (explicitRefunds) { + return If(ValueGT(amount, Constant(0n)), refund(who, amount, continuation), continuation); + } + return continuation; +}; + +const refundUpToBothDeposits = function (who: Party, amount: Value, continuation: Contract): Contract { + if (explicitRefunds) { + return refund(who, Cond(ValueGT(amount, bothDeposits), bothDeposits, amount), continuation); + } + return continuation; +}; + +const refundAfterDifference = function ( + payer: Party, + payerDeposit: Value, + payee: Party, + payeeDeposit: Value, + difference: Value +): Contract { + return refundIfGtZero( + payer, + SubValue(payerDeposit, difference), + refundUpToBothDeposits(payee, AddValue(payeeDeposit, difference), Close) + ); +}; + +export const contractForDifferencesWithOracle: Contract = initialDeposit( + party, + partyDeposit, + TimeParam('Party deposit deadline'), + Close, + initialDeposit( + counterparty, + counterpartyDeposit, + TimeParam('Counterparty deposit deadline'), + refund(party, partyDeposit, Close), + wait( + TimeParam('First window beginning'), + oracleInput( + exchangeBeginning, + TimeParam('First window deadline'), + refundBoth, + wait( + TimeParam('Second window beginning'), + oracleInput( + exchangeEnd, + TimeParam('Second window deadline'), + refundBoth, + recordEndPrice( + priceEnd, + exchangeBeginning, + exchangeEnd, + gtLtEq( + priceBeginning, + UseValue(priceEnd), + recordDifference( + decreaseInPrice, + priceBeginning, + UseValue(priceEnd), + transferUpToDeposit( + counterparty, + counterpartyDeposit, + party, + UseValue(decreaseInPrice), + refundAfterDifference( + counterparty, + counterpartyDeposit, + party, + partyDeposit, + UseValue(decreaseInPrice) + ) + ) + ), + recordDifference( + increaseInPrice, + UseValue(priceEnd), + priceBeginning, + transferUpToDeposit( + party, + partyDeposit, + counterparty, + UseValue(increaseInPrice), + refundAfterDifference( + party, + partyDeposit, + counterparty, + counterpartyDeposit, + UseValue(increaseInPrice) + ) + ) + ), + refundBoth + ) + ) + ) + ) + ) + ) + ) +); diff --git a/src/language/legacy/examples/couponBondGuaranteed.ts b/src/language/legacy/examples/couponBondGuaranteed.ts new file mode 100644 index 00000000..04ff811b --- /dev/null +++ b/src/language/legacy/examples/couponBondGuaranteed.ts @@ -0,0 +1,124 @@ +/* eslint-disable max-params */ +/* eslint-disable sort-keys-fix/sort-keys-fix */ +import { + AddValue, + Case, + Close, + Constant, + ConstantParam, + Contract, + Deposit, + ETimeout, + MulValue, + Party, + Pay, + Role, + SomeNumber, + Value, + When, + ada +} from '../dsl' + +/** + * Marlowe Example : Coupon Bond Guaranteed + * Description : + * Debt agreement between an "Lender" and an "Borrower". + * "Lender" will advance the "Principal" amount at the beginning of the contract, + * and the "Borrower" will pay back "Interest instalment" every 30 slots and the + * "Principal" amount by the end of 3 instalments. The debt is backed by a + * collateral provided by the "Guarantor" which will be refunded as long as the + * "Borrower" pays back on time. + */ + +/* We can set explicitRefunds true to run Close refund analysis + but we get a shorter contract if we set it to false */ +const explicitRefunds: Boolean = false; + +const guarantor: Party = Role('Guarantor'); +const investor: Party = Role('Lender'); +const issuer: Party = Role('Borrower'); + +const principal: Value = ConstantParam('Principal'); +const instalment: Value = ConstantParam('Interest instalment'); + +const guaranteedAmount = function (instalments: SomeNumber): Value { + return AddValue(MulValue(Constant(instalments), instalment), principal); +}; + +const lastInstalment: Value = AddValue(instalment, principal); + +const deposit = function ( + amount: Value, + by: Party, + toAccount: Party, + timeout: ETimeout, + timeoutContinuation: Contract, + continuation: Contract +): Contract { + return When([Case(Deposit(toAccount, by, ada, amount), continuation)], timeout, timeoutContinuation); +}; + +const refundGuarantor = function (amount: Value, continuation: Contract): Contract { + return Pay(investor, Party(guarantor), ada, amount, continuation); +}; + +const transfer = function ( + amount: Value, + from: Party, + to: Party, + timeout: ETimeout, + timeoutContinuation: Contract, + continuation: Contract +): Contract { + return deposit(amount, from, to, timeout, timeoutContinuation, Pay(to, Party(to), ada, amount, continuation)); +}; + +const giveCollateralToLender = function (amount: Value): Contract { + if (explicitRefunds) { + return Pay(investor, Party(investor), ada, amount, Close); + } + return Close; +}; + +export const couponBondGuaranteed: Contract = deposit( + guaranteedAmount(3n), + guarantor, + investor, + 300n, + Close, + transfer( + principal, + investor, + issuer, + 600n, + refundGuarantor(guaranteedAmount(3n), Close), + transfer( + instalment, + issuer, + investor, + 900n, + giveCollateralToLender(guaranteedAmount(3n)), + refundGuarantor( + instalment, + transfer( + instalment, + issuer, + investor, + 1200n, + giveCollateralToLender(guaranteedAmount(2n)), + refundGuarantor( + instalment, + transfer( + lastInstalment, + issuer, + investor, + 1500n, + giveCollateralToLender(guaranteedAmount(1n)), + refundGuarantor(lastInstalment, Close) + ) + ) + ) + ) + ) + ) +); diff --git a/src/language/legacy/examples/escrow.ts b/src/language/legacy/examples/escrow.ts new file mode 100644 index 00000000..4e93161c --- /dev/null +++ b/src/language/legacy/examples/escrow.ts @@ -0,0 +1,100 @@ +/* eslint-disable sort-keys-fix/sort-keys-fix */ +import { + Account, + Bound, + Case, + Choice, + ChoiceId, + Close, + ConstantParam, + Contract, + Deposit, + Party, + Pay, + Role, + SomeNumber, + TimeParam, + Timeout, + Value, + When, + ada +} from '../dsl' + +/** + * Marlowe Example : Escrow + * Description : + * Regulates a money exchange between a "Buyer" and a "Seller". + * If there is a disagreement, an "Mediator" will decide whether the money is refunded or paid to the "Seller". + */ + +/* We can set explicitRefunds true to run Close refund analysis + but we get a shorter contract if we set it to false */ +const explicitRefunds: Boolean = false; + +const buyer: Party = Role('Buyer'); +const seller: Party = Role('Seller'); +const arbiter: Party = Role('Mediator'); + +const price: Value = ConstantParam('Price'); + +const depositTimeout: Timeout = TimeParam('Payment deadline'); +const disputeTimeout: Timeout = TimeParam('Complaint deadline'); +const answerTimeout: Timeout = TimeParam('Complaint response deadline'); +const arbitrageTimeout: Timeout = TimeParam('Mediation deadline'); + +const choice = function (choiceName: string, chooser: Party, choiceValue: SomeNumber, continuation: Contract): Case { + return Case(Choice(ChoiceId(choiceName, chooser), [Bound(choiceValue, choiceValue)]), continuation); +}; + +const deposit = function (timeout: Timeout, timeoutContinuation: Contract, continuation: Contract): Contract { + return When([Case(Deposit(seller, buyer, ada, price), continuation)], timeout, timeoutContinuation); +}; + +const choices = function ( + timeout: Timeout, + chooser: Party, + timeoutContinuation: Contract, + list: { value: SomeNumber; name: string; continuation: Contract }[] +): Contract { + const caseList: Case[] = Array.from({ length: list.length }); + for (const [index, element] of list.entries()) + caseList[index] = choice(element.name, chooser, element.value, element.continuation); + return When(caseList, timeout, timeoutContinuation); +}; + +const sellerToBuyer = function (continuation: Contract): Contract { + return Pay(seller, Account(buyer), ada, price, continuation); +}; + +const paySeller = function (continuation: Contract): Contract { + return Pay(buyer, Party(seller), ada, price, continuation); +}; + +const refundBuyer: Contract = explicitRefunds ? Pay(buyer, Party(buyer), ada, price, Close) : Close; + +const refundSeller: Contract = explicitRefunds ? Pay(seller, Party(seller), ada, price, Close) : Close; + +export const escrow: Contract = deposit( + depositTimeout, + Close, + choices(disputeTimeout, buyer, refundSeller, [ + { value: 0n, name: 'Everything is alright', continuation: refundSeller }, + { + value: 1n, + name: 'Report problem', + continuation: sellerToBuyer( + choices(answerTimeout, seller, refundBuyer, [ + { value: 1n, name: 'Confirm problem', continuation: refundBuyer }, + { + value: 0n, + name: 'Dispute problem', + continuation: choices(arbitrageTimeout, arbiter, refundBuyer, [ + { value: 0n, name: 'Dismiss claim', continuation: paySeller(Close) }, + { value: 1n, name: 'Confirm problem', continuation: refundBuyer } + ]) + } + ]) + ) + } + ]) +); diff --git a/src/language/legacy/examples/escrowWithCollateral.ts b/src/language/legacy/examples/escrowWithCollateral.ts new file mode 100644 index 00000000..fe24b459 --- /dev/null +++ b/src/language/legacy/examples/escrowWithCollateral.ts @@ -0,0 +1,140 @@ +/* eslint-disable sort-keys-fix/sort-keys-fix */ +import { + Account, + Address, + Bound, + Case, + Choice, + ChoiceId, + Close, + ConstantParam, + Contract, + Deposit, + Party, + Pay, + Role, + SomeNumber, + TimeParam, + Timeout, + Value, + When, + ada +} from '../dsl' + +/** + * Marlowe Example : Escrow with Collateral + * Description : + * Regulates a money exchange between a "Buyer" and a "Seller" using a collateral + * from both parties to incentivize collaboration. + * If there is a disagreement the collateral is burned. + */ + +/* We can set explicitRefunds true to run Close refund analysis + but we get a shorter contract if we set it to false */ +const explicitRefunds: Boolean = false; + +const buyer: Party = Role('Buyer'); +const seller: Party = Role('Seller'); +const burnAddress: Party = Address('0000000000000000000000000000000000000000000000000000000000000000'); + +const price: Value = ConstantParam('Price'); +const collateral: Value = ConstantParam('Collateral amount'); + +const sellerCollateralTimeout: Timeout = TimeParam('Collateral deposit by seller timeout'); +const buyerCollateralTimeout: Timeout = TimeParam('Deposit of collateral by buyer timeout'); +const depositTimeout: Timeout = TimeParam('Deposit of price by buyer timeout'); +const disputeTimeout: Timeout = TimeParam('Dispute by buyer timeout'); +const answerTimeout: Timeout = TimeParam('Complaint deadline'); + +const depositCollateral = function ( + party: Party, + timeout: Timeout, + timeoutContinuation: Contract, + continuation: Contract +): Contract { + return When([Case(Deposit(party, party, ada, collateral), continuation)], timeout, timeoutContinuation); +}; + +const burnCollaterals = function (continuation: Contract): Contract { + return Pay( + seller, + Party(burnAddress), + ada, + collateral, + Pay(buyer, Party(burnAddress), ada, collateral, continuation) + ); +}; + +const deposit = function (timeout: Timeout, timeoutContinuation: Contract, continuation: Contract): Contract { + return When([Case(Deposit(seller, buyer, ada, price), continuation)], timeout, timeoutContinuation); +}; + +const choice = function (choiceName: string, chooser: Party, choiceValue: SomeNumber, continuation: Contract): Case { + return Case(Choice(ChoiceId(choiceName, chooser), [Bound(choiceValue, choiceValue)]), continuation); +}; + +const choices = function ( + timeout: Timeout, + chooser: Party, + timeoutContinuation: Contract, + list: { value: SomeNumber; name: string; continuation: Contract }[] +): Contract { + const caseList: Case[] = Array.from({ length: list.length }); + for (const [index, element] of list.entries()) + caseList[index] = choice(element.name, chooser, element.value, element.continuation); + return When(caseList, timeout, timeoutContinuation); +}; + +const sellerToBuyer = function (continuation: Contract): Contract { + return Pay(seller, Account(buyer), ada, price, continuation); +}; + +const refundSellerCollateral = function (continuation: Contract): Contract { + if (explicitRefunds) { + return Pay(seller, Party(seller), ada, collateral, continuation); + } + return continuation; +}; + +const refundBuyerCollateral = function (continuation: Contract): Contract { + if (explicitRefunds) { + return Pay(buyer, Party(buyer), ada, collateral, continuation); + } + return continuation; +}; + +const refundCollaterals = function (continuation: Contract): Contract { + return refundSellerCollateral(refundBuyerCollateral(continuation)); +}; + +const refundBuyer: Contract = explicitRefunds ? Pay(buyer, Party(buyer), ada, price, Close) : Close; + +const refundSeller: Contract = explicitRefunds ? Pay(seller, Party(seller), ada, price, Close) : Close; + +export const escrowWithCollateral: Contract = depositCollateral( + seller, + sellerCollateralTimeout, + Close, + depositCollateral( + buyer, + buyerCollateralTimeout, + refundSellerCollateral(Close), + deposit( + depositTimeout, + refundCollaterals(Close), + choices(disputeTimeout, buyer, refundCollaterals(refundSeller), [ + { value: 0n, name: 'Everything is alright', continuation: refundCollaterals(refundSeller) }, + { + value: 1n, + name: 'Report problem', + continuation: sellerToBuyer( + choices(answerTimeout, seller, refundCollaterals(refundBuyer), [ + { value: 1n, name: 'Confirm problem', continuation: refundCollaterals(refundBuyer) }, + { value: 0n, name: 'Dispute problem', continuation: burnCollaterals(refundBuyer) } + ]) + ) + } + ]) + ) + ) +); diff --git a/src/language/legacy/examples/index.ts b/src/language/legacy/examples/index.ts new file mode 100644 index 00000000..21f055df --- /dev/null +++ b/src/language/legacy/examples/index.ts @@ -0,0 +1,7 @@ +export * from './contractForDifferences'; +export * from './contractForDifferencesWithOracle'; +export * from './couponBondGuaranteed'; +export * from './escrow'; +export * from './escrowWithCollateral'; +export * from './swap'; +export * from './zeroCouponBond'; diff --git a/src/language/legacy/examples/swap.ts b/src/language/legacy/examples/swap.ts new file mode 100644 index 00000000..cf95fb45 --- /dev/null +++ b/src/language/legacy/examples/swap.ts @@ -0,0 +1,89 @@ +/* eslint-disable sort-keys-fix/sort-keys-fix */ +import { + Case, + Close, + Constant, + Contract, + Deposit, + MulValue, + Party, + Pay, + Role, + Timeout, + Token, + Value, + When, + ada +} from '../dsl' + +/** + * Marlowe Example : Swap + * Description : + * Takes Ada from one party and dollar tokens from another party, and it swaps them atomically. + */ + +export const swap = function (adaDepositTimeout:Timeout, tokenDepositTimeout:Timeout,amountOfADA:Value,amountOfToken:Value,token:Token): Contract { + const adaProvider = adaSwapProvider(amountOfADA); + const tokenProvider = tokenSwapProvider(amountOfToken,token); + return makeDeposit( + adaProvider, + adaDepositTimeout, + Close, + makeDeposit( + tokenProvider, + tokenDepositTimeout, + refundSwapParty(adaProvider), + makePayment(adaProvider, tokenProvider, makePayment(tokenProvider, adaProvider, Close)) + )); + }; + +/* We can set explicitRefunds true to run Close refund analysis +but we get a shorter contract if we set it to false */ +const explicitRefunds: Boolean = false; + +const lovelacePerAda: Value = Constant(1_000_000n); +const amountOfLovelace = ( amountOfAda: Value): Value => MulValue(lovelacePerAda, amountOfAda); + + +interface SwapParty { + party: Party; + currency: Token; + amount: Value; +}; + +const adaSwapProvider = ( amountOfAda: Value): SwapParty => ({ + party: Role('Ada provider'), + currency: ada, + amount: amountOfLovelace (amountOfAda) +}); + +const tokenSwapProvider = (amountofToken:Value ,token : Token) : SwapParty => ({ + party: Role('Token provider'), + currency: token, + amount: amountofToken +}); + +const makeDeposit = function ( + src: SwapParty, + timeout: Timeout, + timeoutContinuation: Contract, + continuation: Contract +): Contract { + return When( + [Case(Deposit(src.party, src.party, src.currency, src.amount), continuation)], + timeout, + timeoutContinuation + ); +}; + +const refundSwapParty = function (party: SwapParty): Contract { + if (explicitRefunds) { + return Pay(party.party, Party(party.party), party.currency, party.amount, Close); + } + return Close; +}; + +const makePayment = function (src: SwapParty, dest: SwapParty, continuation: Contract): Contract { + return Pay(src.party, Party(dest.party), src.currency, src.amount, continuation); +}; + diff --git a/src/language/legacy/examples/zeroCouponBond.ts b/src/language/legacy/examples/zeroCouponBond.ts new file mode 100644 index 00000000..47b30989 --- /dev/null +++ b/src/language/legacy/examples/zeroCouponBond.ts @@ -0,0 +1,50 @@ +/* eslint-disable sort-keys-fix/sort-keys-fix */ +import { + AddValue, + Case, + Close, + ConstantParam, + Contract, + Deposit, + Party, + Pay, + Role, + TimeParam, + Timeout, + Value, + When, + ada +} from '../dsl' + +/** + * Marlowe Example : Zero Coupon Bond + * Description : + * A simple loan. The investor pays the issuer + * the discounted price at the start, and is repaid + * the full (notional) price at the end. + */ + +const discountedPrice: Value = ConstantParam('Amount'); +const notionalPrice: Value = AddValue(ConstantParam('Interest'), discountedPrice); + +const investor: Party = Role('Lender'); +const issuer: Party = Role('Borrower'); + +const initialExchange: Timeout = TimeParam('Loan deadline'); +const maturityExchangeTimeout: Timeout = TimeParam('Payback deadline'); + +const transfer = function (timeout: Timeout, from: Party, to: Party, amount: Value, continuation: Contract): Contract { + return When( + [Case(Deposit(from, from, ada, amount), Pay(from, Party(to), ada, amount, continuation))], + timeout, + Close + ); +}; + +export const zeroCouponBond: Contract = transfer( + initialExchange, + investor, + issuer, + discountedPrice, + transfer(maturityExchangeTimeout, issuer, investor, notionalPrice, Close) +); diff --git a/src/runtime/common/address.ts b/src/runtime/common/address.ts new file mode 100644 index 00000000..047a9e47 --- /dev/null +++ b/src/runtime/common/address.ts @@ -0,0 +1,8 @@ +import * as t from "io-ts"; +import { iso, Newtype } from "newtype-ts"; +import { fromNewtype } from "io-ts-types"; + +export type AddressBech32 = Newtype<{ readonly AddressBech32: unique symbol }, string> +export const AddressBech32 = fromNewtype(t.string) +export const unAddressBech32 = iso().unwrap +export const addressBech32 = iso().wrap \ No newline at end of file diff --git a/src/runtime/common/block.ts b/src/runtime/common/block.ts new file mode 100644 index 00000000..f2f0abc1 --- /dev/null +++ b/src/runtime/common/block.ts @@ -0,0 +1,25 @@ + + +import * as t from "io-ts"; +import { identity } from 'fp-ts/lib/function' +import { failure, success, Type } from 'io-ts' +import {PositiveBigInt} from 'io-ts-bigint' + + +export function isBigIntOrNumber(u: unknown): u is (bigint | number) { + return typeof u === 'bigint' || typeof u === 'number' +} + +export const bigint = new Type( + 'bigint', + isBigIntOrNumber, + (i, c) => (isBigIntOrNumber(i) ? success(i) : failure(i, c)), + ((number) => BigInt(number)) + ) + +export type BlockHeader = t.TypeOf +export const BlockHeader = t.type({ slotNo:bigint + , blockNo:bigint + , blockHeaderHash:t.string}) + + \ No newline at end of file diff --git a/src/runtime/common/codec.ts b/src/runtime/common/codec.ts new file mode 100644 index 00000000..a393b83b --- /dev/null +++ b/src/runtime/common/codec.ts @@ -0,0 +1 @@ +export type DecodingError = string[] \ No newline at end of file diff --git a/src/runtime/common/http.ts b/src/runtime/common/http.ts new file mode 100644 index 00000000..e7bc590f --- /dev/null +++ b/src/runtime/common/http.ts @@ -0,0 +1,26 @@ + +/* eslint-disable sort-keys-fix/sort-keys-fix */ +/* eslint-disable no-use-before-define */ +import axios from 'axios'; +import { AxiosInstance, AxiosResponse } from 'axios'; +import * as TE from 'fp-ts/TaskEither' +import { flow, identity } from 'fp-ts/lib/function'; + + +const getOnlyData = TE.bimap( + (e: unknown) => (e instanceof Error ? e : new Error(JSON.stringify(e))), + (v: AxiosResponse): any => v.data, +); + +const getWithDataAndHeaders = TE.bimap( + (e: unknown) => (e instanceof Error ? e : new Error(String(e))), + (v: AxiosResponse): any => [v.headers,v.data], +); + +export const Get = (request: AxiosInstance) => flow(TE.tryCatchK(request.get, identity), getOnlyData); + +export const GetWithDataAndHeaders = (request: AxiosInstance) => flow(TE.tryCatchK(request.get, identity), getWithDataAndHeaders); + +export const Post = (request: AxiosInstance) => flow(TE.tryCatchK(request.post, identity), getOnlyData); + +export const Put = (request: AxiosInstance) => flow(TE.tryCatchK(request.put, identity), getOnlyData); \ No newline at end of file diff --git a/src/runtime/common/iso8601.ts b/src/runtime/common/iso8601.ts new file mode 100644 index 00000000..d8e33b37 --- /dev/null +++ b/src/runtime/common/iso8601.ts @@ -0,0 +1,10 @@ +import * as t from "io-ts"; +import { pipe } from "fp-ts/lib/function"; +import formatISO from 'date-fns/formatISO' + +export type ISO8601 = t.TypeOf +export const ISO8601 = t.string + + +export const datetoIso8601 = (date:Date):ISO8601 => pipe(date,formatISO) + diff --git a/src/runtime/common/metadata/index.ts b/src/runtime/common/metadata/index.ts new file mode 100644 index 00000000..cc912515 --- /dev/null +++ b/src/runtime/common/metadata/index.ts @@ -0,0 +1,17 @@ +import * as t from "io-ts"; + + +export type MetadatumLabel = t.TypeOf +export const MetadatumLabel = t.union([t.bigint,t.string]) + +export type Metadatum = t.TypeOf +export const Metadatum = t.UnknownRecord + +export type Metadata = t.TypeOf +export const Metadata = t.record(MetadatumLabel, Metadatum) + +export type Tag = t.TypeOf +export const Tag = t.string + +export type Tags = t.TypeOf +export const Tags = t.record(Tag, Metadata) \ No newline at end of file diff --git a/src/runtime/common/metadata/tag.ts b/src/runtime/common/metadata/tag.ts new file mode 100644 index 00000000..83c0cde8 --- /dev/null +++ b/src/runtime/common/metadata/tag.ts @@ -0,0 +1,9 @@ +import * as t from "io-ts"; +import { Metadata } from "."; + + +export type Tag = t.TypeOf +export const Tag = t.string + +export type Tags = t.TypeOf +export const Tags = t.record(Tag, Metadata) \ No newline at end of file diff --git a/src/runtime/common/policyId.ts b/src/runtime/common/policyId.ts new file mode 100644 index 00000000..7b4ef1aa --- /dev/null +++ b/src/runtime/common/policyId.ts @@ -0,0 +1,8 @@ +import * as t from "io-ts"; +import { iso, Newtype } from "newtype-ts"; +import { fromNewtype } from "io-ts-types"; + +export type PolicyId = Newtype<{ readonly PolicyId: unique symbol }, string> +export const PolicyId = fromNewtype(t.string) +export const unPolicyId = iso().unwrap +export const policyId = iso().wrap diff --git a/src/runtime/common/state.ts b/src/runtime/common/state.ts new file mode 100644 index 00000000..e2f02c67 --- /dev/null +++ b/src/runtime/common/state.ts @@ -0,0 +1,9 @@ +import * as t from "io-ts"; + +import { Accounts } from "../../language/core/v1/semantics/contract/common/payee/account"; + +export type MarloweState = t.TypeOf +export const MarloweState = t.type({ accounts:Accounts + , boundValues: t.array(t.bigint) + , choices:t.array(t.bigint) + , minTime:t.bigint}) \ No newline at end of file diff --git a/src/runtime/common/textEnvelope.ts b/src/runtime/common/textEnvelope.ts new file mode 100644 index 00000000..c58bfa52 --- /dev/null +++ b/src/runtime/common/textEnvelope.ts @@ -0,0 +1,15 @@ +import reporter from 'io-ts-reporters' +import { Newtype } from "newtype-ts"; +import { fromNewtype,option, optionFromNullable } from "io-ts-types"; +import * as t from "io-ts"; + +// see : https://input-output-hk.github.io/cardano-node/cardano-api/lib/Cardano-Api-SerialiseTextEnvelope.html + +export type TextEnvelope = t.TypeOf +export const TextEnvelope = t.type({ type:t.string, description:t.string, cborHex:t.string}) + +export type MarloweTxCBORHex = string; +export type HexTransactionWitnessSet = string + +export const transactionWitnessSetTextEnvelope : (hexTransactionWitnessSet: HexTransactionWitnessSet) => TextEnvelope + = (hexTransactionWitnessSet) => ({type:"ShelleyTxWitness BabbageEra", description:"",cborHex:hexTransactionWitnessSet}) \ No newline at end of file diff --git a/src/runtime/common/tx/id.ts b/src/runtime/common/tx/id.ts new file mode 100644 index 00000000..26df99c5 --- /dev/null +++ b/src/runtime/common/tx/id.ts @@ -0,0 +1,4 @@ +import * as t from "io-ts"; + +export type TxId = t.TypeOf +export const TxId = t.string // to refine \ No newline at end of file diff --git a/src/runtime/common/tx/outRef.ts b/src/runtime/common/tx/outRef.ts new file mode 100644 index 00000000..6b0d0ae6 --- /dev/null +++ b/src/runtime/common/tx/outRef.ts @@ -0,0 +1,4 @@ +import * as t from "io-ts"; + +export type TxOutRef = t.TypeOf +export const TxOutRef = t.string // to refine \ No newline at end of file diff --git a/src/runtime/common/version.ts b/src/runtime/common/version.ts new file mode 100644 index 00000000..5a5ce439 --- /dev/null +++ b/src/runtime/common/version.ts @@ -0,0 +1,5 @@ + +import * as t from "io-ts"; + +export type MarloweVersion = t.TypeOf; +export const MarloweVersion = t.literal('v1') \ No newline at end of file diff --git a/src/runtime/common/wallet.ts b/src/runtime/common/wallet.ts new file mode 100644 index 00000000..f0efa3cc --- /dev/null +++ b/src/runtime/common/wallet.ts @@ -0,0 +1,12 @@ +import { optionFromNullable } from "io-ts-types" +import { TxOutRef } from "./tx/outRef" +import { AddressBech32 } from "./address" +import * as t from "io-ts"; + + +export type WalletDetails = t.TypeOf +export const WalletDetails = t.type( + { changeAddress: AddressBech32 + , usedAddresses: optionFromNullable(t.array(AddressBech32)) + , collateralUTxOs: optionFromNullable(t.array(TxOutRef)) + }) \ No newline at end of file diff --git a/src/runtime/contract/details.ts b/src/runtime/contract/details.ts new file mode 100644 index 00000000..a2bb62c4 --- /dev/null +++ b/src/runtime/contract/details.ts @@ -0,0 +1,34 @@ + + +import { ContractId } from "./id"; +import { TextEnvelope } from "@runtime/common/textEnvelope"; +import { optionFromNullable } from "io-ts-types"; +import * as t from "io-ts"; +import { BlockHeader } from "@runtime/common/block"; +import { MarloweVersion } from "@runtime/common/version"; +import { PolicyId } from "@runtime/common/policyId"; +import { Metadata } from "@runtime/common/metadata"; +import { TxStatus } from "./transaction/status"; +import { TxOutRef } from "@runtime/common/tx/outRef"; +import { Contract } from "@language/core/v1/semantics/contract"; +import { MarloweState } from "@runtime/common/state"; + + +export type ContractDetails = t.TypeOf +export const ContractDetails + = t.type( + { contractId: ContractId + , roleTokenMintingPolicyId: PolicyId + , version: MarloweVersion + , status: TxStatus + , block: optionFromNullable(BlockHeader) + , metadata: Metadata + , initialContract: Contract + , currentContract: optionFromNullable(Contract) // 3 actions + , state: optionFromNullable(MarloweState) + , txBody: optionFromNullable(TextEnvelope) + , utxo: optionFromNullable(TxOutRef) + }) + + + diff --git a/src/runtime/contract/endpoints/collection.ts b/src/runtime/contract/endpoints/collection.ts new file mode 100644 index 00000000..c477f2ff --- /dev/null +++ b/src/runtime/contract/endpoints/collection.ts @@ -0,0 +1,106 @@ + +import { AxiosInstance } from 'axios'; + +import * as TE from 'fp-ts/TaskEither' +import { pipe } from 'fp-ts/lib/function'; +import { Newtype, iso } from 'newtype-ts' +import * as HTTP from '@runtime/common/http'; +import { Header } from '../header'; + +import { RolesConfig } from '../role'; + +import { Metadata, Tags } from '@runtime/common/metadata'; + +import { TextEnvelope } from '@runtime/common/textEnvelope'; +import { ContractId } from '../id'; +import * as t from "io-ts"; +import { formatValidationErrors } from 'io-ts-reporters' +import { DecodingError } from '@runtime/common/codec'; +import * as E from 'fp-ts/Either' +import * as A from 'fp-ts/Array' +import { MarloweVersion } from '@runtime/common/version'; +import { unAddressBech32 } from '@runtime/common/address'; + +import { fromNewtype, optionFromNullable } from 'io-ts-types'; +import * as O from 'fp-ts/lib/Option'; +import { Contract } from '@language/core/v1/semantics/contract'; +import { WalletDetails } from '@runtime/common/wallet'; + + +export interface ContractsRange extends Newtype<{ readonly ContractsRange: unique symbol }, string> {} +export const ContractsRange = fromNewtype(t.string) +export const unContractsRange = iso().unwrap +export const contractsRange = iso().wrap + +export type GETHeadersByRange = (rangeOption: O.Option) => TE.TaskEither + +export const getHeadersByRangeViaAxios:(axiosInstance: AxiosInstance) => GETHeadersByRange + = (axiosInstance) => (rangeOption) => + pipe( HTTP.GetWithDataAndHeaders(axiosInstance)( '/contracts',pipe(rangeOption,O.match(() => ({}), range => ({ headers: { Range: unContractsRange(range) }})))) + , TE.map(([headers,data]) => + ({ data:data + , previousRange: headers['prev-range'] + , nextRange : headers['next-range']})) + , TE.chainW((data) => TE.fromEither(E.mapLeft(formatValidationErrors)(GETByRangeRawResponse.decode(data)))) + , TE.map((rawResponse) => + ({ headers: pipe(rawResponse.data.results,A.map((result) => result.resource)) + , previousRange: rawResponse.previousRange + , nextRange : rawResponse.nextRange}))) + +type GETByRangeRawResponse = t.TypeOf; +const GETByRangeRawResponse + = t.type({ data : t.type({ results : t.array(t.type({ links : t.type({ contract:t.string, transactions:t.string}) + , resource: Header}))}) + , previousRange : optionFromNullable(ContractsRange) + , nextRange :optionFromNullable(ContractsRange) + }); + +export type GETByRangeResponse = t.TypeOf; +export const GETByRangeResponse + = t.type({ headers : t.array(Header) + , previousRange : optionFromNullable(ContractsRange) + , nextRange :optionFromNullable(ContractsRange) + }); + +export type POST = ( postContractsRequest: PostContractsRequest + , walletDetails: WalletDetails) => TE.TaskEither + +export type PostContractsRequest = t.TypeOf +export const PostContractsRequest + = t.intersection( + [ t.type({ contract: Contract + , version: MarloweVersion + , tags : Tags + , metadata: Metadata + , minUTxODeposit: t.number}) + , t.partial({roles: RolesConfig}) + ]) + + +export type ContractTextEnvelope = t.TypeOf; +export const ContractTextEnvelope = t.type({ contractId:ContractId, tx : TextEnvelope}) + +export type PostResponse = t.TypeOf; +export const PostResponse = t.type({ + links : t.type({ contract:t.string}), + resource: ContractTextEnvelope + }); + +export const postViaAxios:(axiosInstance: AxiosInstance) => POST + = (axiosInstance) => (postContractsRequest, walletDetails) => + pipe( HTTP.Post (axiosInstance) + ( '/contracts' + , postContractsRequest + , { headers: { + 'Accept': 'application/vendor.iog.marlowe-runtime.contract-tx-json', + 'Content-Type':'application/json', + 'X-Change-Address': unAddressBech32(walletDetails.changeAddress), + 'X-Address' : pipe(walletDetails.usedAddresses , A.fromOption, A.flatten, (a) => a.join(',')), + 'X-Collateral-UTxOs': pipe(walletDetails.collateralUTxOs, A.fromOption, A.flatten, (a) => a.join(','))}}) + , TE.chainW((data) => TE.fromEither(E.mapLeft(formatValidationErrors)(PostResponse.decode(data)))) + , TE.map((payload) => payload.resource)) + + + + + diff --git a/src/runtime/contract/endpoints/singleton.ts b/src/runtime/contract/endpoints/singleton.ts new file mode 100644 index 00000000..52733443 --- /dev/null +++ b/src/runtime/contract/endpoints/singleton.ts @@ -0,0 +1,45 @@ + +import { AxiosInstance } from 'axios'; +import * as E from 'fp-ts/Either' +import * as TE from 'fp-ts/TaskEither' +import { pipe } from 'fp-ts/lib/function'; +import * as HTTP from '@runtime/common/http'; +import { HexTransactionWitnessSet, transactionWitnessSetTextEnvelope } from '@runtime/common/textEnvelope'; +import { ContractDetails } from '../details'; +import * as t from "io-ts"; +import {formatValidationErrors} from 'io-ts-reporters' +import { DecodingError } from '@runtime/common/codec'; +import { ContractId, unContractId } from '../id'; + +export type GET = ( contractId: ContractId) => TE.TaskEither + +type GETPayload = t.TypeOf +const GETPayload = t.type({ links: t.type ({}), resource: ContractDetails}) + +export const getViaAxios:(axiosInstance: AxiosInstance) => GET + = (axiosInstance) => (contractId) => + pipe(HTTP.Get(axiosInstance) + ( contractEndpoint(contractId) + , { headers: { Accept: 'application/json', 'Content-Type':'application/json'}}) + , TE.chainW((data) => TE.fromEither(E.mapLeft(formatValidationErrors)(GETPayload.decode(data)))) + , TE.map((payload) => payload.resource)) + +export type PUT = ( contractId: ContractId + , hexTransactionWitnessSet: HexTransactionWitnessSet) + => TE.TaskEither + +export const putViaAxios:(axiosInstance: AxiosInstance) => PUT + = (axiosInstance) => (contractId, hexTransactionWitnessSet) => + pipe(HTTP.Put(axiosInstance) + ( contractEndpoint(contractId) + , transactionWitnessSetTextEnvelope(hexTransactionWitnessSet) + , { headers: { Accept: 'application/json', 'Content-Type':'application/json'}}) + ) + + + +const contractEndpoint = (contractId: ContractId):string => + (`/contracts/${encodeURIComponent(unContractId(contractId))}`) + + + diff --git a/src/runtime/contract/header.ts b/src/runtime/contract/header.ts new file mode 100644 index 00000000..f8e17761 --- /dev/null +++ b/src/runtime/contract/header.ts @@ -0,0 +1,21 @@ +import { BlockHeader } from "@runtime/common/block"; +import { Metadata } from "@runtime/common/metadata"; +import { MarloweVersion } from "@runtime/common/version"; + +import { TxStatus } from "./transaction/status"; +import { ContractId } from "./id"; +import { optionFromNullable } from "io-ts-types"; +import * as t from "io-ts"; + +import { PolicyId } from "@runtime/common/policyId"; + +export type Header = t.TypeOf +export const Header + = t.type( + { contractId: ContractId + , roleTokenMintingPolicyId: PolicyId + , version: MarloweVersion + , status: TxStatus + , block: optionFromNullable(BlockHeader) + , metadata: Metadata + }) \ No newline at end of file diff --git a/src/runtime/contract/id.ts b/src/runtime/contract/id.ts new file mode 100644 index 00000000..1d68a409 --- /dev/null +++ b/src/runtime/contract/id.ts @@ -0,0 +1,20 @@ +import * as t from "io-ts"; +import { iso, Newtype } from "newtype-ts"; +import { fromNewtype } from "io-ts-types"; +import { split } from "fp-ts/lib/string"; +import { pipe } from "fp-ts/lib/function"; +import { head } from "fp-ts/lib/ReadonlyNonEmptyArray"; +import { TxId } from "@runtime/common/tx/id"; + +export type ContractId = Newtype<{ readonly ContractId: unique symbol }, string> +export const ContractId = fromNewtype(t.string) +export const unContractId = iso().unwrap +export const contractId = iso().wrap + + +export const idToTxId : (contractId : ContractId) => TxId + = (contractId) => + pipe( contractId + , unContractId + , split('#') + , head) \ No newline at end of file diff --git a/src/runtime/contract/role.ts b/src/runtime/contract/role.ts new file mode 100644 index 00000000..77b94873 --- /dev/null +++ b/src/runtime/contract/role.ts @@ -0,0 +1,42 @@ +import { AddressBech32 } from "@runtime/common/address"; +import * as t from "io-ts" +import { PolicyId } from "@language/core/v1/semantics/contract/common/policyId"; +import { optionFromNullable } from "io-ts-types"; + + +export type RoleName = string +export const RoleName = t.string + +export type UsePolicy = t.TypeOf +export const UsePolicy = PolicyId + +export type RoleTokenSimple = t.TypeOf +export const RoleTokenSimple = AddressBech32 + +export type TokenMetadataFile = t.TypeOf +export const TokenMetadataFile + = t.type({ name : t.string + , src : t.string + , mediaType : t.string + }) + +export type TokenMetadata = t.TypeOf +export const TokenMetadata + = t.type({ name : optionFromNullable(t.string) + , image : optionFromNullable(t.string) + , mediaType: t.string + , description:t.string + , files:t.array(TokenMetadataFile)}) + +export type RoleTokenAdvanced = t.TypeOf +export const RoleTokenAdvanced = t.type ({address : AddressBech32, metadata : TokenMetadata}) + + +export type RoleTokenConfig = t.TypeOf +export const RoleTokenConfig = t.union ([RoleTokenSimple,RoleTokenAdvanced]) + +export type Mint = t.TypeOf +export const Mint = t.record(RoleName, RoleTokenConfig) + +export type RolesConfig = t.TypeOf +export const RolesConfig = t.union([UsePolicy,Mint]) \ No newline at end of file diff --git a/src/runtime/contract/transaction/details.ts b/src/runtime/contract/transaction/details.ts new file mode 100644 index 00000000..4c7a56db --- /dev/null +++ b/src/runtime/contract/transaction/details.ts @@ -0,0 +1,38 @@ +import { optionFromNullable } from "io-ts-types"; +import { BlockHeader } from "@runtime/common/block"; +import { Metadata } from "@runtime/common/metadata"; +import { ContractId } from "../id"; +import { TransactionId } from "./id"; +import { TxOutRef } from "@runtime/common/tx/outRef"; +import { TxStatus } from "./status"; +import * as t from "io-ts"; +import { BuiltinByteString, Input } from "@language/core/v1/semantics/contract/when/input"; +import { Tags } from "@runtime/common/metadata/tag"; +import { Contract } from "@language/core/v1/semantics/contract"; +import { MarloweState } from "@runtime/common/state"; +import { TxId } from "@runtime/common/tx/id"; +import { ISO8601 } from "@runtime/common/iso8601"; +import { TextEnvelope } from "@runtime/common/textEnvelope"; + +export type Details = t.TypeOf +export const Details + = t.type( + { contractId: ContractId + , transactionId: TransactionId + , continuations : optionFromNullable(BuiltinByteString) + , tags : Tags + , metadata : Metadata + , status: TxStatus + , block: optionFromNullable(BlockHeader) + , inputUtxo : TxOutRef + , inputs : t.array(Input) + , outputUtxo :optionFromNullable(TxOutRef) + , outputContract : optionFromNullable(Contract) + , outputState : optionFromNullable(MarloweState) + , consumingTx : optionFromNullable(TxId) + , invalidBefore : ISO8601 + , invalidHereafter : ISO8601 + , txBody : optionFromNullable(TextEnvelope) + }) + + diff --git a/src/runtime/contract/transaction/endpoints/collection.ts b/src/runtime/contract/transaction/endpoints/collection.ts new file mode 100644 index 00000000..0d8425fd --- /dev/null +++ b/src/runtime/contract/transaction/endpoints/collection.ts @@ -0,0 +1,106 @@ +import * as t from "io-ts"; +import { Newtype, iso } from 'newtype-ts' +import * as E from 'fp-ts/Either' +import * as A from 'fp-ts/Array' +import * as O from 'fp-ts/lib/Option'; +import * as TE from 'fp-ts/TaskEither' +import { formatValidationErrors } from "io-ts-reporters"; +import { fromNewtype, optionFromNullable } from 'io-ts-types'; +import { pipe } from 'fp-ts/lib/function'; + +import { AxiosInstance } from "axios"; + +import * as HTTP from '@runtime/common/http'; +import { unAddressBech32 } from "@runtime/common/address"; +import { Metadata } from "@runtime/common/metadata"; +import { WalletDetails } from "@runtime/common/wallet"; +import { DecodingError } from "@runtime/common/codec"; +import { TextEnvelope } from "@runtime/common/textEnvelope"; +import { MarloweVersion } from "@runtime/common/version"; +import { ISO8601 } from "@runtime/common/iso8601"; +import { Tags } from "@runtime/common/metadata/tag"; +import { Input } from "@language/core/v1/semantics/contract/when/input"; +import { Header } from "../header"; +import { TransactionId } from ".././id"; +import { ContractId, unContractId } from "../../id"; + +export interface TransactionsRange extends Newtype<{ readonly TransactionsRange: unique symbol }, string> {} +export const TransactionsRange = fromNewtype(t.string) +export const unTransactionsRange = iso().unwrap +export const transactionsRange = iso().wrap + +export type GETHeadersByRange = (contractId: ContractId,rangeOption: O.Option) => TE.TaskEither + +export const getHeadersByRangeViaAxios:(axiosInstance: AxiosInstance) => GETHeadersByRange + = (axiosInstance) => (contractId,rangeOption) => + pipe( HTTP.GetWithDataAndHeaders + (axiosInstance) + (transactionsEndpoint(contractId) + ,pipe(rangeOption,O.match(() => ({}), range => ({ headers: { Range: unTransactionsRange(range) }})))) + , TE.map(([headers,data]) => + ({ data:data + , previousRange: headers['prev-range'] + , nextRange : headers['next-range']})) + , TE.chainW((data) => TE.fromEither(E.mapLeft(formatValidationErrors)(GETByRangeRawResponse.decode(data)))) + , TE.map((rawResponse) => + ({ headers: pipe(rawResponse.data.results,A.map((result) => result.resource)) + , previousRange: rawResponse.previousRange + , nextRange : rawResponse.nextRange}))) + +type GETByRangeRawResponse = t.TypeOf; +const GETByRangeRawResponse + = t.type({ data : t.type({ results : t.array(t.type({ links : t.type({ }) + , resource: Header}))}) + , previousRange : optionFromNullable(TransactionsRange) + , nextRange :optionFromNullable(TransactionsRange) + }); + +export type GETByRangeResponse = t.TypeOf; +export const GETByRangeResponse + = t.type({ headers : t.array(Header) + , previousRange : optionFromNullable(TransactionsRange) + , nextRange :optionFromNullable(TransactionsRange) + }); + +export type TransactionTextEnvelope = t.TypeOf; +export const TransactionTextEnvelope = t.type({ contractId:ContractId, transactionId:TransactionId, tx : TextEnvelope}) + +export type POST = ( contractId:ContractId + , postTransactionsRequest: PostTransactionsRequest + , walletDetails: WalletDetails) => TE.TaskEither + + +export const postViaAxios:(axiosInstance: AxiosInstance) => POST + = (axiosInstance) => (contractId, postTransactionsRequest, walletDetails) => + pipe( HTTP.Post (axiosInstance) + ( transactionsEndpoint(contractId) + , postTransactionsRequest + , { headers: { + 'Accept': 'application/vendor.iog.marlowe-runtime.apply-inputs-tx-json', + 'Content-Type':'application/json', + 'X-Change-Address': unAddressBech32(walletDetails.changeAddress), + 'X-Address' : pipe(walletDetails.usedAddresses , A.fromOption, A.flatten, (a) => a.join(',')), + 'X-Collateral-UTxOs': pipe(walletDetails.collateralUTxOs, A.fromOption, A.flatten, (a) => a.join(','))}}) + , TE.chainW((data) => TE.fromEither(E.mapLeft(formatValidationErrors)(PostResponse.decode(data)))) + , TE.map((payload) => payload.resource)) + +export type PostTransactionsRequest = t.TypeOf +export const PostTransactionsRequest + = t.intersection( + [ t.type({ version: MarloweVersion + , inputs: t.array(Input) + , metadata: Metadata + , tags : Tags + }) + , t.partial({ invalidBefore: ISO8601}) + , t.partial({ invalidHereafter: ISO8601}) + ]) + +export type PostResponse = t.TypeOf; +export const PostResponse = t.type({ + links : t.type({ transaction:t.string}), + resource: TransactionTextEnvelope + }); + +const transactionsEndpoint = (contractId: ContractId):string => + (`/contracts/${encodeURIComponent(unContractId(contractId))}/transactions`) \ No newline at end of file diff --git a/src/runtime/contract/transaction/endpoints/singleton.ts b/src/runtime/contract/transaction/endpoints/singleton.ts new file mode 100644 index 00000000..75bfcd41 --- /dev/null +++ b/src/runtime/contract/transaction/endpoints/singleton.ts @@ -0,0 +1,42 @@ + +import * as TE from 'fp-ts/TaskEither' +import * as E from 'fp-ts/Either' +import * as HTTP from '@runtime/common/http'; +import { pipe } from 'fp-ts/lib/function'; +import { AxiosInstance } from "axios"; +import { TransactionId, unTransactionId } from "../id"; +import { ContractId, unContractId } from "../../id"; +import { DecodingError } from "@runtime/common/codec"; +import * as t from "io-ts"; +import { formatValidationErrors } from "io-ts-reporters"; +import { Details } from "../details"; +import { HexTransactionWitnessSet, transactionWitnessSetTextEnvelope } from "@runtime/common/textEnvelope"; + +export type GET = ( contractId: ContractId, transactionId : TransactionId) => TE.TaskEither + +type GETPayload = t.TypeOf +const GETPayload = t.type({ links: t.type ({}), resource: Details}) + +export const getViaAxios:(axiosInstance: AxiosInstance) => GET + = (axiosInstance) => (contractId,transactionId) => + pipe(HTTP.Get(axiosInstance) + ( endpointURI(contractId,transactionId) + , { headers: { Accept: 'application/json', 'Content-Type':'application/json'}}) + , TE.chainW((data) => TE.fromEither(E.mapLeft(formatValidationErrors)(GETPayload.decode(data)))) + , TE.map((payload) => payload.resource)) + +export type PUT = ( contractId: ContractId + , transactionId : TransactionId + , hexTransactionWitnessSet: HexTransactionWitnessSet) + => TE.TaskEither + +export const putViaAxios:(axiosInstance: AxiosInstance) => PUT + = (axiosInstance) => (contractId,transactionId, hexTransactionWitnessSet) => + pipe(HTTP.Put(axiosInstance) + ( endpointURI(contractId,transactionId) + , transactionWitnessSetTextEnvelope(hexTransactionWitnessSet) + , { headers: { Accept: 'application/json', 'Content-Type':'application/json'}}) + ) + +const endpointURI = (contractId: ContractId, transactionId: TransactionId):string => + (`/contracts/${pipe(contractId,unContractId,encodeURIComponent)}/transactions/${pipe(transactionId,unTransactionId,encodeURIComponent)}`) \ No newline at end of file diff --git a/src/runtime/contract/transaction/header.ts b/src/runtime/contract/transaction/header.ts new file mode 100644 index 00000000..a9e618a5 --- /dev/null +++ b/src/runtime/contract/transaction/header.ts @@ -0,0 +1,24 @@ +import { optionFromNullable } from "io-ts-types"; +import { BlockHeader } from "../../common/block"; +import { Metadata } from "../../common/metadata"; +import { ContractId } from "../id"; +import { TransactionId } from "./id"; +import { TxOutRef } from "../../common/tx/outRef"; +import { TxStatus } from "./status"; +import * as t from "io-ts"; +import { BuiltinByteString } from "../../../language/core/v1/semantics/contract/when/input"; +import { Tags } from "../../common/metadata/tag"; + +export type Header = t.TypeOf +export const Header + = t.type( + { contractId: ContractId + , transactionId: TransactionId + , continuations : optionFromNullable(BuiltinByteString) + , tags : Tags + , metadata : Metadata + , status: TxStatus + , block: optionFromNullable(BlockHeader) + , utxo: optionFromNullable(TxOutRef) + }) + diff --git a/src/runtime/contract/transaction/id.ts b/src/runtime/contract/transaction/id.ts new file mode 100644 index 00000000..35daa7ff --- /dev/null +++ b/src/runtime/contract/transaction/id.ts @@ -0,0 +1,17 @@ + +import * as t from "io-ts"; +import { iso, Newtype } from "newtype-ts"; +import { fromNewtype } from "io-ts-types"; +import { TxId } from "@runtime/common/tx/id"; +import { pipe } from "fp-ts/lib/function"; + +export type TransactionId = Newtype<{ readonly TransactionId: unique symbol }, string> +export const TransactionId = fromNewtype(t.string) +export const unTransactionId = iso().unwrap +export const transactionId = iso().wrap + +export const idToTxId : (transactionId : TransactionId) => TxId + = (transactionId) => + pipe( transactionId + , unTransactionId + ) \ No newline at end of file diff --git a/src/runtime/contract/transaction/status.ts b/src/runtime/contract/transaction/status.ts new file mode 100644 index 00000000..83651d6c --- /dev/null +++ b/src/runtime/contract/transaction/status.ts @@ -0,0 +1,4 @@ +import * as t from "io-ts"; + +export type TxStatus = t.TypeOf +export const TxStatus = t.union([ t.literal('unsigned'),t.literal('submitted'),t.literal('confirmed') ]) diff --git a/src/runtime/contract/withdrawal/details.ts b/src/runtime/contract/withdrawal/details.ts new file mode 100644 index 00000000..6f79a793 --- /dev/null +++ b/src/runtime/contract/withdrawal/details.ts @@ -0,0 +1,28 @@ + +import * as t from "io-ts"; +import { BlockHeader } from "../../common/block"; + +import { WithdrawalId } from "./id"; +import { TxStatus } from "../transaction/status"; +import { PolicyId } from "../../common/policyId"; +import { TxOutRef } from "../../common/tx/outRef"; +import { RoleName } from "../role"; + + +export type PayoutRef = t.TypeOf +export const PayoutRef = t.type( + { contractId : TxOutRef + , payout : TxOutRef + , roleTokenMintingPolicyId : PolicyId + , role : RoleName + }) + +export type Details = t.TypeOf +export const Details + = t.type( + { withdrawalId: WithdrawalId + , status: TxStatus + , block: BlockHeader + , payouts : t.array(PayoutRef) + }) + diff --git a/src/runtime/contract/withdrawal/endpoints/collection.ts b/src/runtime/contract/withdrawal/endpoints/collection.ts new file mode 100644 index 00000000..2284723f --- /dev/null +++ b/src/runtime/contract/withdrawal/endpoints/collection.ts @@ -0,0 +1,100 @@ +/* eslint-disable sort-keys-fix/sort-keys-fix */ +/* eslint-disable no-use-before-define */ +import { AxiosInstance } from 'axios'; + +import * as TE from 'fp-ts/TaskEither' +import { pipe } from 'fp-ts/lib/function'; +import { Newtype, iso } from 'newtype-ts' +import * as HTTP from '../../../common/http'; +import { Header } from '../header'; + + +import { TextEnvelope } from '../../../common/textEnvelope'; + +import * as t from "io-ts"; +import { formatValidationErrors } from 'io-ts-reporters' +import { DecodingError } from '../../../common/codec'; +import * as E from 'fp-ts/Either' +import * as A from 'fp-ts/Array' +import { unAddressBech32 } from '../../../common/address'; + +import { fromNewtype, optionFromNullable } from 'io-ts-types'; +import * as O from 'fp-ts/lib/Option'; + +import { WalletDetails } from '../../../common/wallet'; +import { ContractId } from '../../id'; +import { RoleName } from '../../role'; +import { WithdrawalId } from '../id'; + + +export interface WithdrawalsRange extends Newtype<{ readonly WithdrawalsRange: unique symbol }, string> {} +export const WithdrawalsRange = fromNewtype(t.string) +export const unWithdrawalsRange = iso().unwrap +export const contractsRange = iso().wrap + +export type GETHeadersByRange = (rangeOption: O.Option) => TE.TaskEither + +export const getHeadersByRangeViaAxios:(axiosInstance: AxiosInstance) => GETHeadersByRange + = (axiosInstance) => (rangeOption) => + pipe( HTTP.GetWithDataAndHeaders(axiosInstance)( '/withdrawals',pipe(rangeOption,O.match(() => ({}), range => ({ headers: { Range: unWithdrawalsRange(range) }})))) + , TE.map(([headers,data]) => + ({ data:data + , previousRange: headers['prev-range'] + , nextRange : headers['next-range']})) + , TE.chainW((data) => TE.fromEither(E.mapLeft(formatValidationErrors)(GETByRangeRawResponse.decode(data)))) + , TE.map((rawResponse) => + ({ headers: pipe(rawResponse.data.results,A.map((result) => result.resource)) + , previousRange: rawResponse.previousRange + , nextRange : rawResponse.nextRange}))) + +type GETByRangeRawResponse = t.TypeOf; +const GETByRangeRawResponse + = t.type({ data : t.type({ results : t.array(t.type({ links : t.type({ contract:t.string, transactions:t.string}) + , resource: Header}))}) + , previousRange : optionFromNullable(WithdrawalsRange) + , nextRange :optionFromNullable(WithdrawalsRange) + }); + +export type GETByRangeResponse = t.TypeOf; +export const GETByRangeResponse + = t.type({ headers : t.array(Header) + , previousRange : optionFromNullable(WithdrawalsRange) + , nextRange :optionFromNullable(WithdrawalsRange) + }); + +export type POST = ( postWithdrawalsRequest: PostWithdrawalsRequest + , walletDetails: WalletDetails) => TE.TaskEither + +export type PostWithdrawalsRequest = t.TypeOf +export const PostWithdrawalsRequest + = t.type({ contractId: ContractId + , role: RoleName}) + + +export type WithdrawalTextEnvelope = t.TypeOf; +export const WithdrawalTextEnvelope = t.type({ withdrawalId: WithdrawalId, tx : TextEnvelope}) + +export type PostResponse = t.TypeOf; +export const PostResponse = t.type({ + links : t.type({}), + resource: WithdrawalTextEnvelope + }); + +export const postViaAxios:(axiosInstance: AxiosInstance) => POST + = (axiosInstance) => (postWithdrawalsRequest, walletDetails) => + pipe( HTTP.Post (axiosInstance) + ( '/withdrawals' + , postWithdrawalsRequest + , { headers: { + 'Accept': 'application/vendor.iog.marlowe-runtime.withdraw-tx-json', + 'Content-Type':'application/json', + 'X-Change-Address': unAddressBech32(walletDetails.changeAddress), + 'X-Address' : pipe(walletDetails.usedAddresses , A.fromOption, A.flatten, (a) => a.join(',')), + 'X-Collateral-UTxOs': pipe(walletDetails.collateralUTxOs, A.fromOption, A.flatten, (a) => a.join(','))}}) + , TE.chainW((data) => TE.fromEither(E.mapLeft(formatValidationErrors)(PostResponse.decode(data)))) + , TE.map((payload) => payload.resource)) + + + + + diff --git a/src/runtime/contract/withdrawal/endpoints/singleton.ts b/src/runtime/contract/withdrawal/endpoints/singleton.ts new file mode 100644 index 00000000..b45315ed --- /dev/null +++ b/src/runtime/contract/withdrawal/endpoints/singleton.ts @@ -0,0 +1,44 @@ + +/* eslint-disable sort-keys-fix/sort-keys-fix */ +/* eslint-disable no-use-before-define */ +import { AxiosInstance } from 'axios'; +import * as E from 'fp-ts/Either' +import * as TE from 'fp-ts/TaskEither' +import { pipe } from 'fp-ts/lib/function'; +import * as HTTP from '@runtime/common/http'; +import { HexTransactionWitnessSet, transactionWitnessSetTextEnvelope } from '@runtime/common/textEnvelope'; + +import {formatValidationErrors} from 'io-ts-reporters' +import { DecodingError } from '@runtime/common/codec'; +import { unWithdrawalId, WithdrawalId } from '../id'; +import { Details } from '../details'; + + +export type GET = ( withdrawalId: WithdrawalId) => TE.TaskEither + +export const getViaAxios:(axiosInstance: AxiosInstance) => GET + = (axiosInstance) => (withdrawalId) => + pipe(HTTP.Get(axiosInstance) + ( endpointURI(withdrawalId) + , { headers: { Accept: 'application/json', 'Content-Type':'application/json'}}) + , TE.chainW((data) => TE.fromEither(E.mapLeft(formatValidationErrors)(Details.decode(data))))) + +export type PUT = ( withdrawalId: WithdrawalId + , hexTransactionWitnessSet: HexTransactionWitnessSet) + => TE.TaskEither + +export const putViaAxios:(axiosInstance: AxiosInstance) => PUT + = (axiosInstance) => (withdrawalId, hexTransactionWitnessSet) => + pipe(HTTP.Put(axiosInstance) + ( endpointURI(withdrawalId) + , transactionWitnessSetTextEnvelope(hexTransactionWitnessSet) + , { headers: { Accept: 'application/json', 'Content-Type':'application/json'}}) + ) + + + +const endpointURI = (withdrawalId: WithdrawalId):string => + (`/withdrawals/${encodeURIComponent(unWithdrawalId(withdrawalId))}`) + + + diff --git a/src/runtime/contract/withdrawal/header.ts b/src/runtime/contract/withdrawal/header.ts new file mode 100644 index 00000000..e56f08f6 --- /dev/null +++ b/src/runtime/contract/withdrawal/header.ts @@ -0,0 +1,16 @@ + +import * as t from "io-ts"; +import { BlockHeader } from "../../common/block"; + +import { WithdrawalId } from "./id"; +import { TxStatus } from "../transaction/status"; + + +export type Header = t.TypeOf +export const Header + = t.type( + { withdrawalId: WithdrawalId + , status: TxStatus + , block: BlockHeader + }) + \ No newline at end of file diff --git a/src/runtime/contract/withdrawal/id.ts b/src/runtime/contract/withdrawal/id.ts new file mode 100644 index 00000000..736f240e --- /dev/null +++ b/src/runtime/contract/withdrawal/id.ts @@ -0,0 +1,16 @@ +import * as t from "io-ts"; +import { iso, Newtype } from "newtype-ts"; +import { fromNewtype } from "io-ts-types"; +import { pipe } from "fp-ts/lib/function"; +import { TxId } from "../../common/tx/id"; + +export type WithdrawalId = Newtype<{ readonly WithdrawalId: unique symbol }, string> +export const WithdrawalId = fromNewtype(t.string) +export const unWithdrawalId = iso().unwrap +export const withdrawalId= iso().wrap + + +export const idToTxId : (withdrawalId : WithdrawalId) => TxId + = (withdrawalId) => + pipe( withdrawalId + , unWithdrawalId) \ No newline at end of file diff --git a/src/runtime/endpoints.ts b/src/runtime/endpoints.ts new file mode 100644 index 00000000..487d7652 --- /dev/null +++ b/src/runtime/endpoints.ts @@ -0,0 +1,76 @@ + +import axios from 'axios'; +import * as TE from 'fp-ts/TaskEither' +import * as HTTP from '@runtime/common/http'; +import * as WithdrawalSingleton from '@runtime/contract/withdrawal/endpoints/singleton'; +import * as WithdrawalCollection from '@runtime/contract/withdrawal/endpoints/collection'; +import * as ContractSingleton from '@runtime/contract/endpoints/singleton'; +import * as ContractCollection from '@runtime/contract/endpoints/collection'; +import * as TransactionSingleton from '@runtime/contract/transaction/endpoints/singleton'; +import * as TransactionCollection from '@runtime/contract/transaction/endpoints/collection'; +// import curlirize from 'axios-curlirize'; +import { MarloweJSONCodec } from '@adapter/json'; + + +export interface RestAPI { + healthcheck : () => TE.TaskEither + withdrawals: { + getHeadersByRange: WithdrawalCollection.GETHeadersByRange + post: WithdrawalCollection.POST + withdrawal: { + get: WithdrawalSingleton.GET + put: WithdrawalSingleton.PUT + } + } + contracts: { + getHeadersByRange: ContractCollection.GETHeadersByRange + post: ContractCollection.POST + contract: { + get: ContractSingleton.GET + put: ContractSingleton.PUT + transactions: { + getHeadersByRange: TransactionCollection.GETHeadersByRange + post: TransactionCollection.POST + transaction: { + get: TransactionSingleton.GET + put: TransactionSingleton.PUT + } + } + } + } +} + +export const AxiosRestClient = function (baseURL: string): RestAPI { + const axiosInstance = axios.create( + { baseURL:baseURL + , transformRequest: MarloweJSONCodec.encode + , transformResponse: MarloweJSONCodec.decode + }) + + // curlirize(axiosInstance) // N.B for debugging (display all the calls executed in a "curl-ish" way) + return { + healthcheck: () => HTTP.Get(axiosInstance)('/healthcheck'), + withdrawals: { + getHeadersByRange: WithdrawalCollection.getHeadersByRangeViaAxios(axiosInstance), + post: WithdrawalCollection.postViaAxios(axiosInstance), + withdrawal: { + get: WithdrawalSingleton.getViaAxios(axiosInstance), + put: WithdrawalSingleton.putViaAxios(axiosInstance)}}, + contracts: { + getHeadersByRange: ContractCollection.getHeadersByRangeViaAxios(axiosInstance), + post: ContractCollection.postViaAxios(axiosInstance), + contract: { + get: ContractSingleton.getViaAxios(axiosInstance), + put: ContractSingleton.putViaAxios(axiosInstance), + transactions: { + getHeadersByRange: TransactionCollection.getHeadersByRangeViaAxios(axiosInstance), + post: TransactionCollection.postViaAxios(axiosInstance), + transaction: { + get: TransactionSingleton.getViaAxios(axiosInstance), + put: TransactionSingleton.putViaAxios(axiosInstance) + } + } + } + } + } +} diff --git a/src/runtime/index.ts b/src/runtime/index.ts new file mode 100644 index 00000000..252214f9 --- /dev/null +++ b/src/runtime/index.ts @@ -0,0 +1,2 @@ +export * as RuntimeRest from './endpoints'; +export * as Runtime from './endpoints'; diff --git a/src/runtime/write/command.ts b/src/runtime/write/command.ts new file mode 100644 index 00000000..c3204337 --- /dev/null +++ b/src/runtime/write/command.ts @@ -0,0 +1,74 @@ + +import * as TE from 'fp-ts/TaskEither' +import * as ContractCollection from '@runtime/contract/endpoints/collection'; +import * as TransactionCollection from '@runtime/contract/transaction/endpoints/collection'; +import * as WithdrawalCollection from '@runtime/contract/withdrawal/endpoints/collection'; +import { DecodingError } from '@runtime/common/codec'; +import { ContractDetails } from '@runtime/contract/details'; +import { pipe } from 'fp-ts/function' +import { WalletDetails } from '@runtime/common/wallet'; +import { HexTransactionWitnessSet, MarloweTxCBORHex } from '@runtime/common/textEnvelope'; +import { ContractId } from '@runtime/contract/id'; +import * as Contract from '@runtime/contract/id'; +import * as Tx from '@runtime/contract/transaction/id'; +import * as Transaction from '@runtime/contract/transaction/details'; +import * as Withdrawal from '@runtime/contract/withdrawal/details'; +import * as WithdrawalId from '@runtime/contract/withdrawal/id'; +import { RestAPI } from '@runtime/endpoints'; + + +export type InitialisePayload = ContractCollection.PostContractsRequest +export type ApplyInputsPayload = TransactionCollection.PostTransactionsRequest +export type WithdrawPayload = WithdrawalCollection.PostWithdrawalsRequest + +export const initialise : + (client : RestAPI) + => (waitConfirmation : (txHash : string ) => TE.TaskEither) + => (signAndRetrieveOnlyHexTransactionWitnessSet : (tx :MarloweTxCBORHex) => TE.TaskEither) + => (walletDetails:WalletDetails) + => (payload : InitialisePayload) + => TE.TaskEither + = (client) => (waitConfirmation) => (sign) => (walletDetails) => (payload) => + pipe( client.contracts.post( payload, walletDetails) + , TE.chainW((contractTextEnvelope) => + pipe ( sign(contractTextEnvelope.tx.cborHex) + , TE.chain((hexTransactionWitnessSet) => + client.contracts.contract.put( contractTextEnvelope.contractId, hexTransactionWitnessSet)) + , TE.map (() => contractTextEnvelope.contractId))) + , TE.chainFirstW((contractId) => waitConfirmation(pipe(contractId, Contract.idToTxId))) + , TE.chainW ((contractId) => client.contracts.contract.get(contractId))) + +export const applyInputs : + (client : RestAPI) + => (waitConfirmation : (txHash : string ) => TE.TaskEither) + => (signAndRetrieveOnlyHexTransactionWitnessSet : (tx :MarloweTxCBORHex) => TE.TaskEither) + => (walletDetails:WalletDetails) + => (contractId : ContractId) + => ( payload : ApplyInputsPayload) + => TE.TaskEither + = (client) => (waitConfirmation) => (sign) => (walletDetails) => (contractId) => (payload) => + pipe( client.contracts.contract.transactions.post(contractId, payload, walletDetails) + , TE.chainW((transactionTextEnvelope) => + pipe ( sign(transactionTextEnvelope.tx.cborHex) + , TE.chain((hexTransactionWitnessSet) => + client.contracts.contract.transactions.transaction.put( contractId,transactionTextEnvelope.transactionId, hexTransactionWitnessSet)) + , TE.map (() => transactionTextEnvelope.transactionId))) + , TE.chainFirstW((transactionId) => waitConfirmation(pipe(transactionId, Tx.idToTxId))) + , TE.chainW ((transactionId) => + client.contracts.contract.transactions.transaction.get(contractId,transactionId))) + +export const withdraw : + (client : RestAPI) + => (waitConfirmation : (txHash : string ) => TE.TaskEither) + => (signAndRetrieveOnlyHexTransactionWitnessSet : (tx :MarloweTxCBORHex) => TE.TaskEither) + => (walletDetails:WalletDetails) + => ( payload : WithdrawPayload) + => TE.TaskEither + = (client) => (waitConfirmation) => (sign) => (walletDetails) => (payload) => + pipe( client.withdrawals.post (payload, walletDetails) + , TE.chainW( (withdrawalTextEnvelope) => + pipe ( sign(withdrawalTextEnvelope.tx.cborHex) + , TE.chain ((hexTransactionWitnessSet) => client.withdrawals.withdrawal.put(withdrawalTextEnvelope.withdrawalId,hexTransactionWitnessSet)) + , TE.map (() => withdrawalTextEnvelope.withdrawalId))) + , TE.chainFirstW((withdrawalId) => waitConfirmation(pipe(withdrawalId, WithdrawalId.idToTxId))) + , TE.chainW ((withdrawalId) => client.withdrawals.withdrawal.get(withdrawalId)) ) \ No newline at end of file diff --git a/src/tsconfig.json b/src/tsconfig.json index e16db591..bed65fe2 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -1,3 +1,6 @@ -{ "extends": "../tsconfig.json", +{ + "compilerOptions": { + "module": "esnext" + }, "extends": "../tsconfig.json", "include": [ "." ] } diff --git a/test/global.d.ts b/test/global.d.ts new file mode 100644 index 00000000..df1c9ae6 --- /dev/null +++ b/test/global.d.ts @@ -0,0 +1,3 @@ +import '@relmify/jest-fp-ts'; + + diff --git a/test/language/core/v1/examples/jsons/contractForDifferences.json b/test/language/core/v1/examples/jsons/contractForDifferences.json new file mode 100644 index 00000000..d065c38b --- /dev/null +++ b/test/language/core/v1/examples/jsons/contractForDifferences.json @@ -0,0 +1,219 @@ +{ + "when": [{ + "then": { + "when": [{ + "then": { + "when": [], + "timeout_continuation": { + "when": [{ + "then": { + "when": [], + "timeout_continuation": { + "when": [{ + "then": { + "then": { + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "account": { + "role_token": "Party" + } + }, + "then": "close", + "pay": { + "then": { + "use_value": "Decrease in price" + }, + "if": { + "value": { + "use_value": "Decrease in price" + }, + "lt": 0 + }, + "else": 0 + }, + "from_account": { + "role_token": "Counterparty" + } + }, + "let": "Decrease in price", + "be": { + "value": { + "value_of_choice": { + "choice_owner": { + "role_token": "Oracle" + }, + "choice_name": "Price in first window" + } + }, + "minus": { + "value_of_choice": { + "choice_owner": { + "role_token": "Oracle" + }, + "choice_name": "Price in second window" + } + } + } + }, + "if": { + "value": { + "value_of_choice": { + "choice_owner": { + "role_token": "Oracle" + }, + "choice_name": "Price in first window" + } + }, + "gt": { + "value_of_choice": { + "choice_owner": { + "role_token": "Oracle" + }, + "choice_name": "Price in second window" + } + } + }, + "else": { + "then": { + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "account": { + "role_token": "Counterparty" + } + }, + "then": "close", + "pay": { + "then": { + "use_value": "Increase in price" + }, + "if": { + "value": { + "use_value": "Increase in price" + }, + "lt": 0 + }, + "else": 0 + }, + "from_account": { + "role_token": "Party" + } + }, + "let": "Increase in price", + "be": { + "value": { + "value_of_choice": { + "choice_owner": { + "role_token": "Oracle" + }, + "choice_name": "Price in second window" + } + }, + "minus": { + "value_of_choice": { + "choice_owner": { + "role_token": "Oracle" + }, + "choice_name": "Price in first window" + } + } + } + }, + "if": { + "value": { + "value_of_choice": { + "choice_owner": { + "role_token": "Oracle" + }, + "choice_name": "Price in first window" + } + }, + "lt": { + "value_of_choice": { + "choice_owner": { + "role_token": "Oracle" + }, + "choice_name": "Price in second window" + } + } + }, + "else": "close" + } + }, + "case": { + "for_choice": { + "choice_owner": { + "role_token": "Oracle" + }, + "choice_name": "Price in second window" + }, + "choose_between": [{ + "to": 1000000000, + "from": 0 + }] + } + }], + "timeout_continuation": "close", + "timeout": 1679043795918 + }, + "timeout": 1679043495918 + }, + "case": { + "for_choice": { + "choice_owner": { + "role_token": "Oracle" + }, + "choice_name": "Price in first window" + }, + "choose_between": [{ + "to": 1000000000, + "from": 0 + }] + } + }], + "timeout_continuation": "close", + "timeout": 1679043195918 + }, + "timeout": 1679042895918 + }, + "case": { + "party": { + "role_token": "Counterparty" + }, + "of_token": { + "token_name": "", + "currency_symbol": "" + }, + "into_account": { + "role_token": "Counterparty" + }, + "deposits": 0 + } + }], + "timeout_continuation": "close", + "timeout": 1679042595918 + }, + "case": { + "party": { + "role_token": "Party" + }, + "of_token": { + "token_name": "", + "currency_symbol": "" + }, + "into_account": { + "role_token": "Party" + }, + "deposits": 0 + } + }], + "timeout_continuation": "close", + "timeout": 1679042295918 +} \ No newline at end of file diff --git a/test/language/core/v1/examples/jsons/contractForDifferencesWithOracle.json b/test/language/core/v1/examples/jsons/contractForDifferencesWithOracle.json new file mode 100644 index 00000000..b31a462b --- /dev/null +++ b/test/language/core/v1/examples/jsons/contractForDifferencesWithOracle.json @@ -0,0 +1,198 @@ +{ + "when": [{ + "then": { + "when": [{ + "then": { + "when": [], + "timeout_continuation": { + "when": [{ + "then": { + "when": [], + "timeout_continuation": { + "when": [{ + "then": { + "then": { + "then": { + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "account": { + "role_token": "Party" + } + }, + "then": "close", + "pay": { + "then": { + "use_value": "Decrease in price" + }, + "if": { + "value": { + "use_value": "Decrease in price" + }, + "lt": 0 + }, + "else": 0 + }, + "from_account": { + "role_token": "Counterparty" + } + }, + "let": "Decrease in price", + "be": { + "value": 0, + "minus": { + "use_value": "Price in second window" + } + } + }, + "if": { + "value": 0, + "gt": { + "use_value": "Price in second window" + } + }, + "else": { + "then": { + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "account": { + "role_token": "Counterparty" + } + }, + "then": "close", + "pay": { + "then": { + "use_value": "Increase in price" + }, + "if": { + "value": { + "use_value": "Increase in price" + }, + "lt": 0 + }, + "else": 0 + }, + "from_account": { + "role_token": "Party" + } + }, + "let": "Increase in price", + "be": { + "value": { + "use_value": "Price in second window" + }, + "minus": 0 + } + }, + "if": { + "value": 0, + "lt": { + "use_value": "Price in second window" + } + }, + "else": "close" + } + }, + "let": "Price in second window", + "be": { + "divide": { + "times": { + "times": { + "value_of_choice": { + "choice_owner": { + "role_token": "kraken" + }, + "choice_name": "inv-adausd" + } + }, + "multiply": { + "value_of_choice": { + "choice_owner": { + "role_token": "kraken" + }, + "choice_name": "dir-adausd" + } + } + }, + "multiply": 0 + }, + "by": 10000000000000000 + } + }, + "case": { + "for_choice": { + "choice_owner": { + "role_token": "kraken" + }, + "choice_name": "inv-adausd" + }, + "choose_between": [{ + "to": 100000000000, + "from": 0 + }] + } + }], + "timeout_continuation": "close", + "timeout": 1679043829460 + }, + "timeout": 1679043529460 + }, + "case": { + "for_choice": { + "choice_owner": { + "role_token": "kraken" + }, + "choice_name": "dir-adausd" + }, + "choose_between": [{ + "to": 100000000000, + "from": 0 + }] + } + }], + "timeout_continuation": "close", + "timeout": 1679043229460 + }, + "timeout": 1679042929460 + }, + "case": { + "party": { + "role_token": "Counterparty" + }, + "of_token": { + "token_name": "", + "currency_symbol": "" + }, + "into_account": { + "role_token": "Counterparty" + }, + "deposits": 0 + } + }], + "timeout_continuation": "close", + "timeout": 1679042629460 + }, + "case": { + "party": { + "role_token": "Party" + }, + "of_token": { + "token_name": "", + "currency_symbol": "" + }, + "into_account": { + "role_token": "Party" + }, + "deposits": 0 + } + }], + "timeout_continuation": "close", + "timeout": 1679042329460 +} \ No newline at end of file diff --git a/test/language/core/v1/examples/jsons/couponBondGuaranteed.json b/test/language/core/v1/examples/jsons/couponBondGuaranteed.json new file mode 100644 index 00000000..879e1b47 --- /dev/null +++ b/test/language/core/v1/examples/jsons/couponBondGuaranteed.json @@ -0,0 +1,237 @@ +{ + "when": [{ + "then": { + "when": [{ + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "party": { + "role_token": "Borrower" + } + }, + "then": { + "when": [{ + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "party": { + "role_token": "Lender" + } + }, + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "party": { + "role_token": "Guarantor" + } + }, + "then": { + "when": [{ + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "party": { + "role_token": "Lender" + } + }, + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "party": { + "role_token": "Guarantor" + } + }, + "then": { + "when": [{ + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "party": { + "role_token": "Lender" + } + }, + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "party": { + "role_token": "Guarantor" + } + }, + "then": "close", + "pay": { + "and": 0, + "add": 0 + }, + "from_account": { + "role_token": "Lender" + } + }, + "pay": { + "and": 0, + "add": 0 + }, + "from_account": { + "role_token": "Lender" + } + }, + "case": { + "party": { + "role_token": "Borrower" + }, + "of_token": { + "token_name": "", + "currency_symbol": "" + }, + "into_account": { + "role_token": "Lender" + }, + "deposits": { + "and": 0, + "add": 0 + } + } + }], + "timeout_continuation": "close", + "timeout": 1500 + }, + "pay": 0, + "from_account": { + "role_token": "Lender" + } + }, + "pay": 0, + "from_account": { + "role_token": "Lender" + } + }, + "case": { + "party": { + "role_token": "Borrower" + }, + "of_token": { + "token_name": "", + "currency_symbol": "" + }, + "into_account": { + "role_token": "Lender" + }, + "deposits": 0 + } + }], + "timeout_continuation": "close", + "timeout": 1200 + }, + "pay": 0, + "from_account": { + "role_token": "Lender" + } + }, + "pay": 0, + "from_account": { + "role_token": "Lender" + } + }, + "case": { + "party": { + "role_token": "Borrower" + }, + "of_token": { + "token_name": "", + "currency_symbol": "" + }, + "into_account": { + "role_token": "Lender" + }, + "deposits": 0 + } + }], + "timeout_continuation": "close", + "timeout": 900 + }, + "pay": 0, + "from_account": { + "role_token": "Borrower" + } + }, + "case": { + "party": { + "role_token": "Lender" + }, + "of_token": { + "token_name": "", + "currency_symbol": "" + }, + "into_account": { + "role_token": "Borrower" + }, + "deposits": 0 + } + }], + "timeout_continuation": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "party": { + "role_token": "Guarantor" + } + }, + "then": "close", + "pay": { + "and": 0, + "add": { + "times": 0, + "multiply": 3 + } + }, + "from_account": { + "role_token": "Lender" + } + }, + "timeout": 600 + }, + "case": { + "party": { + "role_token": "Guarantor" + }, + "of_token": { + "token_name": "", + "currency_symbol": "" + }, + "into_account": { + "role_token": "Lender" + }, + "deposits": { + "and": 0, + "add": { + "times": 0, + "multiply": 3 + } + } + } + }], + "timeout_continuation": "close", + "timeout": 300 +} \ No newline at end of file diff --git a/test/language/core/v1/examples/jsons/escrow.json b/test/language/core/v1/examples/jsons/escrow.json new file mode 100644 index 00000000..b2707b13 --- /dev/null +++ b/test/language/core/v1/examples/jsons/escrow.json @@ -0,0 +1,146 @@ +{ + "when": [{ + "then": { + "when": [{ + "then": "close", + "case": { + "for_choice": { + "choice_owner": { + "role_token": "Buyer" + }, + "choice_name": "Everything is alright" + }, + "choose_between": [{ + "to": 0, + "from": 0 + }] + } + }, { + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "account": { + "role_token": "Buyer" + } + }, + "then": { + "when": [{ + "then": "close", + "case": { + "for_choice": { + "choice_owner": { + "role_token": "Seller" + }, + "choice_name": "Confirm problem" + }, + "choose_between": [{ + "to": 1, + "from": 1 + }] + } + }, { + "then": { + "when": [{ + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "party": { + "role_token": "Seller" + } + }, + "then": "close", + "pay": 12000000, + "from_account": { + "role_token": "Buyer" + } + }, + "case": { + "for_choice": { + "choice_owner": { + "role_token": "Mediator" + }, + "choice_name": "Dismiss claim" + }, + "choose_between": [{ + "to": 0, + "from": 0 + }] + } + }, { + "then": "close", + "case": { + "for_choice": { + "choice_owner": { + "role_token": "Mediator" + }, + "choice_name": "Confirm problem" + }, + "choose_between": [{ + "to": 1, + "from": 1 + }] + } + }], + "timeout_continuation": "close", + "timeout": 1679038507831 + }, + "case": { + "for_choice": { + "choice_owner": { + "role_token": "Seller" + }, + "choice_name": "Dispute problem" + }, + "choose_between": [{ + "to": 0, + "from": 0 + }] + } + }], + "timeout_continuation": "close", + "timeout": 1679038207831 + }, + "pay": 12000000, + "from_account": { + "role_token": "Seller" + } + }, + "case": { + "for_choice": { + "choice_owner": { + "role_token": "Buyer" + }, + "choice_name": "Report problem" + }, + "choose_between": [{ + "to": 1, + "from": 1 + }] + } + }], + "timeout_continuation": "close", + "timeout": 1679037907831 + }, + "case": { + "party": { + "role_token": "Buyer" + }, + "of_token": { + "token_name": "", + "currency_symbol": "" + }, + "into_account": { + "role_token": "Seller" + }, + "deposits": 12000000 + } + }], + "timeout_continuation": "close", + "timeout": 1679037607831 +} \ No newline at end of file diff --git a/test/language/core/v1/examples/jsons/escrowWithCollateral.json b/test/language/core/v1/examples/jsons/escrowWithCollateral.json new file mode 100644 index 00000000..eba750bd --- /dev/null +++ b/test/language/core/v1/examples/jsons/escrowWithCollateral.json @@ -0,0 +1,167 @@ +{ + "when": [{ + "then": { + "when": [{ + "then": { + "when": [{ + "then": { + "when": [{ + "then": "close", + "case": { + "for_choice": { + "choice_owner": { + "role_token": "Buyer" + }, + "choice_name": "Everything is alright" + }, + "choose_between": [{ + "to": 0, + "from": 0 + }] + } + }, { + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "account": { + "role_token": "Buyer" + } + }, + "then": { + "when": [{ + "then": "close", + "case": { + "for_choice": { + "choice_owner": { + "role_token": "Seller" + }, + "choice_name": "Confirm problem" + }, + "choose_between": [{ + "to": 1, + "from": 1 + }] + } + }, { + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "party": { + "address": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "party": { + "address": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + "then": "close", + "pay": 0, + "from_account": { + "role_token": "Buyer" + } + }, + "pay": 0, + "from_account": { + "role_token": "Seller" + } + }, + "case": { + "for_choice": { + "choice_owner": { + "role_token": "Seller" + }, + "choice_name": "Dispute problem" + }, + "choose_between": [{ + "to": 0, + "from": 0 + }] + } + }], + "timeout_continuation": "close", + "timeout": 1679038852138 + }, + "pay": 0, + "from_account": { + "role_token": "Seller" + } + }, + "case": { + "for_choice": { + "choice_owner": { + "role_token": "Buyer" + }, + "choice_name": "Report problem" + }, + "choose_between": [{ + "to": 1, + "from": 1 + }] + } + }], + "timeout_continuation": "close", + "timeout": 1679038552138 + }, + "case": { + "party": { + "role_token": "Buyer" + }, + "of_token": { + "token_name": "", + "currency_symbol": "" + }, + "into_account": { + "role_token": "Seller" + }, + "deposits": 0 + } + }], + "timeout_continuation": "close", + "timeout": 1679038252138 + }, + "case": { + "party": { + "role_token": "Buyer" + }, + "of_token": { + "token_name": "", + "currency_symbol": "" + }, + "into_account": { + "role_token": "Buyer" + }, + "deposits": 0 + } + }], + "timeout_continuation": "close", + "timeout": 1679037952138 + }, + "case": { + "party": { + "role_token": "Seller" + }, + "of_token": { + "token_name": "", + "currency_symbol": "" + }, + "into_account": { + "role_token": "Seller" + }, + "deposits": 0 + } + }], + "timeout_continuation": "close", + "timeout": 1679037652138 +} \ No newline at end of file diff --git a/test/language/core/v1/examples/jsons/swap.json b/test/language/core/v1/examples/jsons/swap.json new file mode 100644 index 00000000..bd0963bb --- /dev/null +++ b/test/language/core/v1/examples/jsons/swap.json @@ -0,0 +1,75 @@ +{ + "when": [{ + "then": { + "when": [{ + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "party": { + "role_token": "Dollar provider" + } + }, + "then": { + "token": { + "token_name": "dollar", + "currency_symbol": "85bb65" + }, + "to": { + "party": { + "role_token": "Ada provider" + } + }, + "then": "close", + "pay": 0, + "from_account": { + "role_token": "Dollar provider" + } + }, + "pay": { + "times": 0, + "multiply": 1000000 + }, + "from_account": { + "role_token": "Ada provider" + } + }, + "case": { + "party": { + "role_token": "Dollar provider" + }, + "of_token": { + "token_name": "dollar", + "currency_symbol": "85bb65" + }, + "into_account": { + "role_token": "Dollar provider" + }, + "deposits": 0 + } + }], + "timeout_continuation": "close", + "timeout": 1678970171344 + }, + "case": { + "party": { + "role_token": "Ada provider" + }, + "of_token": { + "token_name": "", + "currency_symbol": "" + }, + "into_account": { + "role_token": "Ada provider" + }, + "deposits": { + "times": 0, + "multiply": 1000000 + } + } + }], + "timeout_continuation": "close", + "timeout": 1678969871344 +} \ No newline at end of file diff --git a/test/language/core/v1/examples/jsons/zeroCouponBond.json b/test/language/core/v1/examples/jsons/zeroCouponBond.json new file mode 100644 index 00000000..234b886b --- /dev/null +++ b/test/language/core/v1/examples/jsons/zeroCouponBond.json @@ -0,0 +1,75 @@ +{ + "when": [{ + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "party": { + "role_token": "Borrower" + } + }, + "then": { + "when": [{ + "then": { + "token": { + "token_name": "", + "currency_symbol": "" + }, + "to": { + "party": { + "role_token": "Lender" + } + }, + "then": "close", + "pay": { + "and": 0, + "add": 0 + }, + "from_account": { + "role_token": "Borrower" + } + }, + "case": { + "party": { + "role_token": "Borrower" + }, + "of_token": { + "token_name": "", + "currency_symbol": "" + }, + "into_account": { + "role_token": "Borrower" + }, + "deposits": { + "and": 0, + "add": 0 + } + } + }], + "timeout_continuation": "close", + "timeout": 1679042482797 + }, + "pay": 0, + "from_account": { + "role_token": "Lender" + } + }, + "case": { + "party": { + "role_token": "Lender" + }, + "of_token": { + "token_name": "", + "currency_symbol": "" + }, + "into_account": { + "role_token": "Lender" + }, + "deposits": 0 + } + }], + "timeout_continuation": "close", + "timeout": 1679042182797 +} \ No newline at end of file diff --git a/test/language/core/v1/examples/parsing.spec.ts b/test/language/core/v1/examples/parsing.spec.ts new file mode 100644 index 00000000..5a00dd5b --- /dev/null +++ b/test/language/core/v1/examples/parsing.spec.ts @@ -0,0 +1,44 @@ + +import * as TE from 'fp-ts/TaskEither' +import * as E from 'fp-ts/Either' + +import '@relmify/jest-fp-ts' +import { pipe } from 'fp-ts/lib/function'; +import {formatValidationErrors} from 'io-ts-reporters' +import {Contract} from '../../../../../src/language/core/v1/semantics/contract' +import * as path from 'path' +import { fileURLToPath } from 'url'; +import {MarloweJSONCodec, minify} from '../../../../../src/adapter/json' +import { getFileContents } from '../../../../../src/adapter/file'; + + + +const getfilename = () => fileURLToPath(import.meta.url); +export const currentDirectoryPath = () => path.dirname(getfilename()); + +describe('examples', () => { + +it.each([ 'swap' + , 'escrow' + , 'escrowWithCollateral' + , 'contractForDifferences' + , 'contractForDifferencesWithOracle' + , 'zeroCouponBond' + , 'couponBondGuaranteed' + ]) + ('(%p) can be decoded/encoded and is isomorphic', async (filename) => { + + await pipe( TE.Do + , TE.bind('uncoded', () => getFileContents(path.join(currentDirectoryPath(), `/jsons/${filename}.json`))) + , TE.bind('decoded', ({uncoded}) => TE.of(MarloweJSONCodec.decode(uncoded))) + , TE.bindW('typed', ({decoded}) => + TE.fromEither(pipe( Contract.decode(decoded) + , E.mapLeft(formatValidationErrors)))) + , TE.bindW('encoded', ({typed}) => TE.of(MarloweJSONCodec.encode(typed))) + , TE.match( + (e) => { console.dir(e, { depth: null }); expect(e).not.toBeDefined()}, + ({encoded,uncoded}) => {expect(minify(encoded)).toEqual(minify(uncoded))})) () + + }) +}) + diff --git a/test/language/core/v1/semantics/contract/common/payee/accounts/jsons/one-account-with-ada.json b/test/language/core/v1/semantics/contract/common/payee/accounts/jsons/one-account-with-ada.json new file mode 100644 index 00000000..8f77933e --- /dev/null +++ b/test/language/core/v1/semantics/contract/common/payee/accounts/jsons/one-account-with-ada.json @@ -0,0 +1,14 @@ +[ + [ + [ + { + "address": "addr_test1vqpxg5welxsfg8m7gf76unura6esfctexsg0sa8kx4u207ga5zckw" + }, + { + "currency_symbol": "", + "token_name": "" + } + ], + 3000000 + ] + ] \ No newline at end of file diff --git a/test/language/core/v1/semantics/contract/common/payee/accounts/parsing.spec.ts b/test/language/core/v1/semantics/contract/common/payee/accounts/parsing.spec.ts new file mode 100644 index 00000000..dcb3035c --- /dev/null +++ b/test/language/core/v1/semantics/contract/common/payee/accounts/parsing.spec.ts @@ -0,0 +1,38 @@ + +import * as TE from 'fp-ts/TaskEither' +import * as E from 'fp-ts/Either' + +import { pipe } from 'fp-ts/lib/function'; +import {formatValidationErrors} from 'io-ts-reporters' +import {Value} from '../../../../../../../../../src/language/core/v1/semantics/contract/common/value' +import * as path from 'path' +import { MarloweJSONCodec, minify } from '../../../../../../../../../src/adapter/json'; +import { getFileContents } from '../../../../../../../../../src/adapter/file'; +import { fileURLToPath } from 'url'; +import { Accounts } from '../../../../../../../../../src/language/core/v1/semantics/contract/common/payee/account'; + + +const getfilename = () => fileURLToPath(import.meta.url); +export const currentDirectoryPath = () => path.dirname(getfilename()); + +describe('Accounts', () => { + + it.each(['one-account-with-ada']) + ('(%p) can be decoded/encoded and is isomorphic', async (filename) => { + + await pipe( TE.Do + , TE.bind('uncoded', () => getFileContents(path.join(currentDirectoryPath(), `/jsons/${filename}.json`))) + , TE.bind('decoded', ({uncoded}) => TE.of(MarloweJSONCodec.decode(uncoded))) + , TE.bindW('typed', ({decoded}) => + TE.fromEither(pipe( Accounts.decode(decoded) + , E.mapLeft(formatValidationErrors)))) + , TE.bindW('encoded', ({typed}) => TE.of(MarloweJSONCodec.encode(typed))) + , TE.match( + (e) => { console.dir(e, { depth: null }); expect(e).not.toBeDefined()}, + ({encoded,uncoded}) => {expect(minify(encoded)).toEqual(minify(uncoded))})) () + + }) + +}) + + diff --git a/test/language/core/v1/semantics/contract/common/value/jsons/value.json b/test/language/core/v1/semantics/contract/common/value/jsons/value.json new file mode 100644 index 00000000..ffa90ed8 --- /dev/null +++ b/test/language/core/v1/semantics/contract/common/value/jsons/value.json @@ -0,0 +1,24 @@ + { + "divide": { + "times": { + "times": { + "value_of_choice": { + "choice_owner": { + "role_token": "kraken" + }, + "choice_name": "inv-adausd" + } + }, + "multiply": { + "value_of_choice": { + "choice_owner": { + "role_token": "kraken" + }, + "choice_name": "dir-adausd" + } + } + }, + "multiply": 0 + }, + "by": 10000000000000000 +} \ No newline at end of file diff --git a/test/language/core/v1/semantics/contract/common/value/parsing.spec.ts b/test/language/core/v1/semantics/contract/common/value/parsing.spec.ts new file mode 100644 index 00000000..ad86cdf3 --- /dev/null +++ b/test/language/core/v1/semantics/contract/common/value/parsing.spec.ts @@ -0,0 +1,37 @@ + +import * as TE from 'fp-ts/TaskEither' +import * as E from 'fp-ts/Either' + +import { pipe } from 'fp-ts/lib/function'; +import {formatValidationErrors} from 'io-ts-reporters' +import {Value} from '../../../../../../../../src/language/core/v1/semantics/contract/common/value' +import * as path from 'path' +import { MarloweJSONCodec, minify } from '../../../../../../../../src/adapter/json'; +import { getFileContents } from '../../../../../../../../src/adapter/file'; +import { fileURLToPath } from 'url'; + + +const getfilename = () => fileURLToPath(import.meta.url); +export const currentDirectoryPath = () => path.dirname(getfilename()); + +describe('value', () => { + + it.each(['value']) + ('(%p) can be decoded/encoded and is isomorphic', async (filename) => { + + await pipe( TE.Do + , TE.bind('uncoded', () => getFileContents(path.join(currentDirectoryPath(), `/jsons/${filename}.json`))) + , TE.bind('decoded', ({uncoded}) => TE.of(MarloweJSONCodec.decode(uncoded))) + , TE.bindW('typed', ({decoded}) => + TE.fromEither(pipe( Value.decode(decoded) + , E.mapLeft(formatValidationErrors)))) + , TE.bindW('encoded', ({typed}) => TE.of(MarloweJSONCodec.encode(typed))) + , TE.match( + (e) => { console.dir(e, { depth: null }); expect(e).not.toBeDefined()}, + ({encoded,uncoded}) => {expect(minify(encoded)).toEqual(minify(uncoded))})) () + + }) + +}) + + diff --git a/test/language/core/v1/semantics/contract/jsons/close.json b/test/language/core/v1/semantics/contract/jsons/close.json new file mode 100644 index 00000000..fbd0dd02 --- /dev/null +++ b/test/language/core/v1/semantics/contract/jsons/close.json @@ -0,0 +1 @@ +"close" \ No newline at end of file diff --git a/test/language/core/v1/semantics/contract/jsons/let.json b/test/language/core/v1/semantics/contract/jsons/let.json new file mode 100644 index 00000000..8f3e98cd --- /dev/null +++ b/test/language/core/v1/semantics/contract/jsons/let.json @@ -0,0 +1,6 @@ +{ + "let": "Price in second window", + "be": 0, + "then":"close" + +} \ No newline at end of file diff --git a/test/language/core/v1/semantics/contract/jsons/pay.json b/test/language/core/v1/semantics/contract/jsons/pay.json new file mode 100644 index 00000000..d81570d5 --- /dev/null +++ b/test/language/core/v1/semantics/contract/jsons/pay.json @@ -0,0 +1,16 @@ +{ + "token": { + "token_name": "dollar", + "currency_symbol": "85bb65" + }, + "to": { + "party": { + "role_token": "Ada provider" + } + }, + "then": "close", + "pay": 0, + "from_account": { + "role_token": "Dollar provider" + } +} \ No newline at end of file diff --git a/test/language/core/v1/semantics/contract/jsons/when.json b/test/language/core/v1/semantics/contract/jsons/when.json new file mode 100644 index 00000000..cf8f735c --- /dev/null +++ b/test/language/core/v1/semantics/contract/jsons/when.json @@ -0,0 +1,5 @@ +{ + "when": [], + "timeout_continuation": "close", + "timeout": 1678969871344 +} \ No newline at end of file diff --git a/test/language/core/v1/semantics/contract/parsing.spec.ts b/test/language/core/v1/semantics/contract/parsing.spec.ts new file mode 100644 index 00000000..7967c91b --- /dev/null +++ b/test/language/core/v1/semantics/contract/parsing.spec.ts @@ -0,0 +1,45 @@ + +import * as TE from 'fp-ts/TaskEither' +import * as E from 'fp-ts/Either' +import * as T from 'fp-ts/Task' + +import fs from 'fs'; +import '@relmify/jest-fp-ts' +import * as O from 'fp-ts/lib/Option'; +import { promisify } from 'util'; +import { pipe } from 'fp-ts/lib/function'; +import {formatValidationErrors} from 'io-ts-reporters' +import {Contract} from '../../../../../../src/language/core/v1/semantics/contract' +import * as path from 'path' +import { fileURLToPath } from 'url'; +import {MarloweJSONCodec, minify} from '../../../../../../src/adapter/json' +import { getFileContents } from '../../../../../../src/adapter/file'; + +const getfilename = () => fileURLToPath(import.meta.url); +export const currentDirectoryPath = () => path.dirname(getfilename()); + +describe('contract', () => { + + it.each(['close' + ,'when' + ,'pay' + ,'let' + ]) + ('(%p) can be decoded/encoded and is isomorphic', async (filename) => { + + await pipe( TE.Do + , TE.bind('uncoded', () => getFileContents(path.join(currentDirectoryPath(), `/jsons/${filename}.json`))) + , TE.bind('decoded', ({uncoded}) => TE.of(MarloweJSONCodec.decode(uncoded))) + , TE.bindW('typed', ({decoded}) => + TE.fromEither(pipe( Contract.decode(decoded) + , E.mapLeft(formatValidationErrors)))) + , TE.bindW('encoded', ({typed}) => TE.of(MarloweJSONCodec.encode(typed))) + , TE.match( + (e) => { console.dir(e, { depth: null }); expect(e).not.toBeDefined()}, + ({encoded,uncoded}) => {expect(minify(encoded)).toEqual(minify(uncoded))})) () + + }) + +}) + + diff --git a/test/language/core/v1/semantics/contract/when/action/jsons/deposit.json b/test/language/core/v1/semantics/contract/when/action/jsons/deposit.json new file mode 100644 index 00000000..9def070b --- /dev/null +++ b/test/language/core/v1/semantics/contract/when/action/jsons/deposit.json @@ -0,0 +1,16 @@ +{ + "party": { + "role_token": "Ada provider" + }, + "of_token": { + "token_name": "", + "currency_symbol": "" + }, + "into_account": { + "role_token": "Ada provider" + }, + "deposits": { + "times": 0, + "multiply": 1000000 + } +} diff --git a/test/language/core/v1/semantics/contract/when/action/parsing.spec.ts b/test/language/core/v1/semantics/contract/when/action/parsing.spec.ts new file mode 100644 index 00000000..f74b30ca --- /dev/null +++ b/test/language/core/v1/semantics/contract/when/action/parsing.spec.ts @@ -0,0 +1,37 @@ + +import * as TE from 'fp-ts/TaskEither' +import * as E from 'fp-ts/Either' + +import '@relmify/jest-fp-ts' +import { pipe } from 'fp-ts/lib/function'; +import {formatValidationErrors} from 'io-ts-reporters' +import {Action} from '../../../../../../../../src/language/core/v1/semantics/contract/when/action/' +import * as path from 'path' +import { MarloweJSONCodec, minify } from '../../../../../../../../src/adapter/json'; +import { getFileContents } from '../../../../../../../../src/adapter/file'; +import { fileURLToPath } from 'url'; + +const getfilename = () => fileURLToPath(import.meta.url); +export const currentDirectoryPath = () => path.dirname(getfilename()); + +describe('contract', () => { + + it.each(['deposit']) + ('(%p) can be decoded/encoded and is isomorphic', async (filename) => { + + await pipe( TE.Do + , TE.bind('uncoded', () => getFileContents(path.join(currentDirectoryPath(), `/jsons/${filename}.json`))) + , TE.bind('decoded', ({uncoded}) => TE.of(MarloweJSONCodec.decode(uncoded))) + , TE.bindW('typed', ({decoded}) => + TE.fromEither(pipe( Action.decode(decoded) + , E.mapLeft(formatValidationErrors)))) + , TE.bindW('encoded', ({typed}) => TE.of(MarloweJSONCodec.encode(typed))) + , TE.match( + (e) => { console.dir(e, { depth: null }); expect(e).not.toBeDefined()}, + ({encoded,uncoded}) => {expect(minify(encoded)).toEqual(minify(uncoded))})) () + + }) + +}) + + diff --git a/test/runtime/context.ts b/test/runtime/context.ts new file mode 100644 index 00000000..9b31f21d --- /dev/null +++ b/test/runtime/context.ts @@ -0,0 +1,21 @@ +import { Network } from "lucid-cardano"; +import { Context, getPrivateKeyFromHexString } from "../../src/adapter/wallet/lucid"; + + +export function getBlockfrostContext () : Context { + const { BLOCKFROST_URL, BLOCKFROST_PROJECT_ID, NETWORK_ID } = process.env; + return new Context ( BLOCKFROST_PROJECT_ID as string + , BLOCKFROST_URL as string + , NETWORK_ID as Network); + }; + +export function getBankPrivateKey () : string { + const { BANK_PK_HEX } = process.env; + return getPrivateKeyFromHexString(BANK_PK_HEX as string) +} + +export function getMarloweRuntimeUrl () : string { + const { MARLOWE_WEB_SERVER_URL} = process.env; + return MARLOWE_WEB_SERVER_URL as string +}; + \ No newline at end of file diff --git a/test/runtime/endpoints/contracts.spec.e2e.ts b/test/runtime/endpoints/contracts.spec.e2e.ts new file mode 100644 index 00000000..6cb17631 --- /dev/null +++ b/test/runtime/endpoints/contracts.spec.e2e.ts @@ -0,0 +1,83 @@ + +import { pipe } from 'fp-ts/function' +import * as O from 'fp-ts/lib/Option'; +import * as TE from 'fp-ts/TaskEither' +import { close } from '../../../src/language/core/v1/semantics/contract/close' +import { AxiosRestClient } from '../../../src/runtime/endpoints'; +import { initialise } from '../../../src/runtime/write/command'; +import { initialiseBankAndverifyProvisionning } from '../provisionning' +import { getBankPrivateKey, getBlockfrostContext, getMarloweRuntimeUrl } from '../context'; + + +describe('contracts endpoints', () => { + + const restApi = AxiosRestClient(getMarloweRuntimeUrl()) + + it(' can build a Tx for Initialising a Marlowe Contract' + + '(can POST: /contracts/ )', async () => { + await + pipe( initialiseBankAndverifyProvisionning + (getMarloweRuntimeUrl()) + (getBlockfrostContext ()) + (getBankPrivateKey()) + , TE.bind('postContractResponse',({bank}) => + restApi.contracts.post( { contract: close + , version: 'v1' + , metadata: {} + , tags : {} + , minUTxODeposit: 2_000_000} + , { changeAddress: bank.address + , usedAddresses: O.none + , collateralUTxOs: O.none})) + , TE.map (({postContractResponse}) => postContractResponse) + , TE.match( + (e) => { console.dir(e, { depth: null }); expect(e).not.toBeDefined()}, + () => {})) () + + },100_000), + it('can Initialise a Marlowe Contract ' + + '(can POST: /contracts/ => build the Tx server side' + + ' and PUT : /contracts/{contractid} => Append the Contract Tx to the Cardano ledger' + + ' and GET /contracts/{contractid} => provide details about the contract initialised)', async () => { + await + pipe( initialiseBankAndverifyProvisionning + (getMarloweRuntimeUrl()) + (getBlockfrostContext ()) + (getBankPrivateKey()) + , TE.bindW('contractDetails',({bank}) => + initialise + (restApi) + (bank.waitConfirmation) + (bank.signMarloweTx) + ({ changeAddress: bank.address + , usedAddresses: O.none + , collateralUTxOs: O.none}) + ( { contract: close + , version: 'v1' + , metadata: {} + , tags : {} + , minUTxODeposit: 2_000_000})) + , TE.match( + (e) => { console.dir(e, { depth: null }); + expect(e).not.toBeDefined()}, + () => {})) () + + },100_000), + it('can navigate throught Initialised Marlowe Contracts pages' + + '(GET: /contracts/)', async () => { + await + pipe( initialiseBankAndverifyProvisionning + (getMarloweRuntimeUrl()) + (getBlockfrostContext ()) + (getBankPrivateKey()) + , TE.bindW('firstPage' ,() => restApi.contracts.getHeadersByRange(O.none)) + , TE.bindW('secondPage',({firstPage}) => restApi.contracts.getHeadersByRange(firstPage.nextRange)) + , TE.bindW('thirdPage' ,({secondPage}) => restApi.contracts.getHeadersByRange(secondPage.nextRange)) + , TE.match( + (e) => { console.dir(e, { depth: null }); expect(e).not.toBeDefined()}, + () => {})) () + + + },100_000) +}) + diff --git a/test/runtime/endpoints/transactions.spec.e2e.ts b/test/runtime/endpoints/transactions.spec.e2e.ts new file mode 100644 index 00000000..9df45219 --- /dev/null +++ b/test/runtime/endpoints/transactions.spec.e2e.ts @@ -0,0 +1,116 @@ + +import { pipe } from 'fp-ts/function' +import * as TE from 'fp-ts/TaskEither' +import * as O from 'fp-ts/lib/Option'; + +import { getBankPrivateKey, getBlockfrostContext, getMarloweRuntimeUrl } from '../context'; +import { AxiosRestClient } from '../../../src/runtime/endpoints'; +import { addDays } from 'date-fns/fp' +import { datetoTimeout } from '../../../src/language/core/v1/semantics/contract/when' +import * as Contract from '../../../src/runtime/contract/id' +import * as Tx from '../../../src/runtime/contract/transaction/id' +import { addMinutes, subMinutes } from 'date-fns' +import { datetoIso8601 } from '../../../src/runtime/common/iso8601' +import { inputNotify } from '../../../src/language/core/v1/semantics/contract/when/input/notify' +import { initialiseBankAndverifyProvisionning } from '../provisionning' +import { oneNotifyTrue } from '../../../src/language/core/v1/examples/contract-one-notify' + + +describe('Contracts/{contractd}/Transactions endpoints', () => { + + + + it('can Build Apply Input Tx : ' + + '(can POST: /contracts/{contractId}/transactions => ask to build the Tx to apply input on an initialised Marlowe Contract)', async () => { + await + pipe( initialiseBankAndverifyProvisionning + (getMarloweRuntimeUrl()) + (getBlockfrostContext ()) + (getBankPrivateKey()) + , TE.let (`notifyTimeout`, () => pipe(Date.now(),addDays(1),datetoTimeout)) + , TE.bind('result',({restApi, initialise,bank,notifyTimeout}) => + pipe + ( initialise + ( { contract: oneNotifyTrue(notifyTimeout) + , version: 'v1' + , metadata: {} + , tags : {} + , minUTxODeposit: 2_000_000}) + , TE.chainW ((contractDetails) => + restApi.contracts.contract.transactions.post + (contractDetails.contractId + , { version : "v1" + , inputs : [inputNotify] + , metadata : {} + , tags : {} + , invalidBefore : pipe(Date.now(),(date) => subMinutes(date,5),datetoIso8601) + , invalidHereafter : pipe(Date.now(),(date) => addMinutes(date,5),datetoIso8601) } + , { changeAddress: bank.address + , usedAddresses: O.none + , collateralUTxOs: O.none}) ) + )) + , TE.map (({result}) => result) + , TE.match( + (e) => { console.dir(e, { depth: null }); expect(e).not.toBeDefined()}, + () => {}) + ) () + + },100_000), + it('can Apply Inputs : ' + + '(can POST: /contracts/{contractId}/transactions => ask to build the Tx to apply input on an initialised Marlowe Contract' + + ' , PUT: /contracts/{contractId}/transactions/{transactionId} => Append the Applied Input Tx to the ledger' + + ' , GET: /contracts/{contractId}/transactions/{transactionId} => retrieve the Tx state' + + ' and GET : /contracts/{contractId}/transactions => should see the unsigned transaction listed)', async () => { + await + pipe( initialiseBankAndverifyProvisionning + (getMarloweRuntimeUrl()) + (getBlockfrostContext ()) + (getBankPrivateKey()) + , TE.let (`notifyTimeout`, () => pipe(Date.now(),addDays(1),datetoTimeout)) + , TE.bind('result',({restApi, initialise,applyInputs,bank,notifyTimeout}) => + pipe + ( initialise + ( { contract: oneNotifyTrue(notifyTimeout) + , version: 'v1' + , metadata: {} + , tags : {} + , minUTxODeposit: 2_000_000}) + , TE.chainW ((contractDetails) => + applyInputs + (contractDetails.contractId) + ({ version : "v1" + , inputs : [inputNotify] + , metadata : {} + , tags : {}})) + , TE.chainFirstW ((txDetails) => + bank.waitConfirmation(pipe(txDetails.transactionId, Tx.idToTxId))) + , TE.chainW ((postResult) => + restApi.contracts.contract.transactions.getHeadersByRange (postResult.contractId,O.none)))) + , TE.map (({result}) => expect(result.headers.length).toBe(1)) + , TE.match( + (e) => { console.dir(e, { depth: null }); expect(e).not.toBeDefined()}, + () => {}) + ) () + },100_000), + + it('can navigate throught Apply Inputs Txs pages ' + + '(GET: /contracts/{contractId}/transactions )', async () => { + await + pipe( initialiseBankAndverifyProvisionning + (getMarloweRuntimeUrl()) + (getBlockfrostContext ()) + (getBankPrivateKey()) + , TE.bindW('firstPage' ,({restApi}) => + restApi.contracts.contract.transactions.getHeadersByRange + (Contract.contractId("e72f18b5ec9afed70171b071192226b2625ca5f21716be8f9028ca392d75e899#1") + ,O.none)) + , TE.match( + (e) => { console.dir(e, { depth: null }); expect(e).not.toBeDefined()}, + () => {})) () + + + },10_000) +}) + + + diff --git a/test/runtime/endpoints/withdrawals.spec.e2e.ts b/test/runtime/endpoints/withdrawals.spec.e2e.ts new file mode 100644 index 00000000..fcd5e163 --- /dev/null +++ b/test/runtime/endpoints/withdrawals.spec.e2e.ts @@ -0,0 +1,108 @@ + + +import { pipe } from 'fp-ts/function' +import * as Examples from '../../../src/language/core/v1/examples' +import { addDays } from 'date-fns/fp' +import * as TE from 'fp-ts/TaskEither' +import * as O from 'fp-ts/Option' +import { getBankPrivateKey, getBlockfrostContext, getMarloweRuntimeUrl } from '../context'; +import { datetoTimeout } from '../../../src/language/core/v1/semantics/contract/when' +import { AxiosRestClient } from '../../../src/runtime/endpoints' +import { provisionAnAdaAndTokenProvider } from '../provisionning' + +describe('withdrawals endpoints ', () => { + + const restApi = AxiosRestClient(getMarloweRuntimeUrl()) + const provisionScheme = + { adaProvider : { adaAmount : 20_000_000n} + , tokenprovider : { adaAmount :20_000_000n + , tokenAmount : 50n + , tokenName : "TokenA" }} + const executeSwapWithRequiredWithdrawalTillClosing + = pipe( provisionAnAdaAndTokenProvider + (getMarloweRuntimeUrl()) + (getBlockfrostContext ()) + (getBankPrivateKey()) + (provisionScheme) + , TE.let (`swapRequest`, ({tokenAsset}) => + ({ adaDepositTimeout : pipe(Date.now(),addDays(1),datetoTimeout) + , tokenDepositTimeout : pipe(Date.now(),addDays(2),datetoTimeout) + , amountOfADA : 2n + , amountOfToken : 3n + , token :tokenAsset })) + , TE.let (`swapWithRequiredWithdrawalAndExpectedInputs`, ({swapRequest}) => + Examples.swapWithRequiredWithdrawalAndExpectedInputs(swapRequest)) + , TE.bindW('contractDetails',({initialise,applyInputs,adaProvider,tokenProvider,swapWithRequiredWithdrawalAndExpectedInputs}) => + pipe( initialise + (adaProvider) + ( { contract: swapWithRequiredWithdrawalAndExpectedInputs.swap + , roles: {'Ada provider' : adaProvider.address + ,'Token provider' : tokenProvider.address} + , version: 'v1' + , metadata: {} + , tags : {} + , minUTxODeposit: 3_000_000}) + , TE.chainW ((contractDetails) => + applyInputs + (adaProvider) + (contractDetails.contractId) + ({ version : "v1" + , inputs : [swapWithRequiredWithdrawalAndExpectedInputs.adaProviderInputDeposit] + , metadata : {} + , tags : {}})) + , TE.chainW ((contractDetails) => + applyInputs + (tokenProvider) + (contractDetails.contractId) + ({ version : "v1" + , inputs : [swapWithRequiredWithdrawalAndExpectedInputs.tokenProviderInputDeposit] + , metadata : {} + , tags : {}}) + ))) ) + + it('can build a withdraw Tx : ' + + '(can POST : /withdrawals => ask to build the Tx to withdraw assets on the closed contract )' , async () => { + + await + pipe( executeSwapWithRequiredWithdrawalTillClosing + , TE.bindW('result',({adaProvider,contractDetails}) => + pipe( restApi.withdrawals.post + ( { contractId : contractDetails.contractId + , role : 'Ada provider' } + , { changeAddress: adaProvider.address + , usedAddresses: O.none + , collateralUTxOs: O.none}) + )) + , TE.match( + (e) => { console.dir(e, { depth: null }); expect(e).not.toBeDefined()}, + (res) => { } )) () + + + },1000_000); + + it('can withdraw : ' + + '(can POST : /withdrawals => ask to build the Tx to withdraw assets on the closed contract )' + + ' and PUT : /withdrawals/{withdrawalId} => Append the withdraw Tx to the ledger' + + ' and GET : /withdrawals/{withdrawalId} => retrieve the Tx state', async () => { + + await + pipe( executeSwapWithRequiredWithdrawalTillClosing + , TE.bindW('result',({adaProvider,tokenProvider,contractDetails,withdraw}) => + pipe + ( withdraw + (adaProvider) + ( { contractId : contractDetails.contractId + , role : 'Ada provider' }) + , TE.chain (() => + withdraw + (tokenProvider) + ( { contractId : contractDetails.contractId + , role : 'Token provider' })) )) + , TE.match( + (e) => { console.dir(e, { depth: null }); expect(e).not.toBeDefined()}, + () => {} )) () + + + },1000_000); +}); + diff --git a/test/runtime/examples/swap.spec.e2e.ts b/test/runtime/examples/swap.spec.e2e.ts new file mode 100644 index 00000000..fbcfb0aa --- /dev/null +++ b/test/runtime/examples/swap.spec.e2e.ts @@ -0,0 +1,67 @@ + + +import { pipe } from 'fp-ts/function' +import * as Examples from '../../../src/language/core/v1/examples' +import { addDays } from 'date-fns/fp' +import * as TE from 'fp-ts/TaskEither' +import { getBankPrivateKey, getBlockfrostContext, getMarloweRuntimeUrl } from '../context'; +import { datetoTimeout } from '../../../src/language/core/v1/semantics/contract/when' +import { provisionAnAdaAndTokenProvider } from '../provisionning' + + +describe('swap', () => { + it('can execute the nominal case', async () => { + + const provisionScheme = + { adaProvider : { adaAmount : 20_000_000n} + , tokenprovider : { adaAmount :20_000_000n + , tokenAmount : 50n + , tokenName : "TokenA" }} + + await + pipe( provisionAnAdaAndTokenProvider + (getMarloweRuntimeUrl()) + (getBlockfrostContext ()) + (getBankPrivateKey()) + (provisionScheme) + , TE.let (`swapRequest`, ({tokenAsset}) => + ({ adaDepositTimeout : pipe(Date.now(),addDays(1),datetoTimeout) + , tokenDepositTimeout : pipe(Date.now(),addDays(2),datetoTimeout) + , amountOfADA : 3n + , amountOfToken : 10n + , token :tokenAsset })) + , TE.let (`swapWithExpectedInputs`, ({swapRequest}) => + Examples.swapWithExpectedInputs(swapRequest)) + , TE.bindW('swapClosedResult',({initialise,applyInputs,adaProvider,tokenProvider,swapWithExpectedInputs}) => + pipe( initialise + (adaProvider) + ( { contract: swapWithExpectedInputs.swap + , roles: {'Ada provider' : adaProvider.address + ,'Token provider' : tokenProvider.address} + , version: 'v1' + , metadata: {} + , tags : {} + , minUTxODeposit: 3_000_000}) + , TE.chainW ((contractDetails) => + applyInputs + (adaProvider) + (contractDetails.contractId) + ({ version : "v1" + , inputs : [swapWithExpectedInputs.adaProviderInputDeposit] + , metadata : {} + , tags : {}})) + , TE.chainW ((contractDetails) => + applyInputs + (tokenProvider) + (contractDetails.contractId) + ({ version : "v1" + , inputs : [swapWithExpectedInputs.tokenProviderInputDeposit] + , metadata : {} + , tags : {}})))) + , TE.match( + (e) => { console.dir(e, { depth: null }); expect(e).not.toBeDefined()}, + () => { } )) () + + },1000_000); +}); + diff --git a/test/runtime/provisionning.ts b/test/runtime/provisionning.ts new file mode 100644 index 00000000..1ed5dd5f --- /dev/null +++ b/test/runtime/provisionning.ts @@ -0,0 +1,113 @@ +import * as TE from 'fp-ts/TaskEither' +import * as T from 'fp-ts/Task' +import * as O from 'fp-ts/Option' +import { pipe } from 'fp-ts/function' +import { Context, SingleAddressWallet } from '../../src/adapter/wallet/lucid' +import {PrivateKeysAsHex} from '../../src/adapter/wallet/privateKeys' +import { log } from '../../src/adapter/logging' +import * as ADA from '../../src/adapter/wallet/ada' +import { token, TokenName } from '../../src/language/core/v1/semantics/contract/common/token' +import { AxiosRestClient } from '../../src/runtime/endpoints' +import { applyInputs, initialise, withdraw } from '../../src/runtime/write/command' + +export type ProvisionScheme = + { adaProvider : {adaAmount : bigint} + , tokenprovider : {adaAmount :bigint,tokenAmount : bigint, tokenName : TokenName} + } + +export const provisionAnAdaAndTokenProvider + = (runtimeURL: string) => + (walletContext: Context) => + (bankPrivateKey : PrivateKeysAsHex) => + (scheme : ProvisionScheme) => + pipe( TE.Do + // Generating/Initialising Accounts + , T.bind('bank',() => SingleAddressWallet.Initialise (walletContext,bankPrivateKey)) + , T.bind('adaProvider',() => SingleAddressWallet.Random(walletContext)) + , T.bind('tokenProvider',() => SingleAddressWallet.Random(walletContext)) + , TE.fromTask + // Check Banks treasury + , TE.bind('bankBalance',({bank}) => bank.adaBalance) + , TE.chainFirst(({bankBalance,bank}) => TE.of(pipe( + log(`Bank (${bank.address})`), + () => log(` - ${ADA.format(bankBalance)}`)))) + , TE.chainFirst(({bankBalance}) => TE.of(expect(bankBalance).toBeGreaterThan(100_000_000))) + // Provisionning + , TE.chainFirst(({bank,adaProvider,tokenProvider}) => + bank.provision([[adaProvider,scheme.adaProvider.adaAmount], + [tokenProvider,scheme.tokenprovider.adaAmount]])) + , TE.bind('tokenAsset',({tokenProvider}) => tokenProvider.mintRandomTokens(scheme.tokenprovider.tokenName,scheme.tokenprovider.tokenAmount)) + // Provisionning Checks + // Ada Provider + , TE.bind('adaProviderBalance',({adaProvider}) => adaProvider.adaBalance) + , TE.chainFirst(({adaProvider,adaProviderBalance}) => TE.of(pipe( + log( `Ada Provider (${adaProvider.address}`), + () => log(` - ${ADA.format(adaProviderBalance)}`)))) + // Token Provider + , TE.bind('tokenProviderADABalance',({tokenProvider}) => tokenProvider.adaBalance) + , TE.bind('tokenProviderMintedTokenBalance' ,({tokenProvider,tokenAsset}) => tokenProvider.assetBalance(tokenAsset)) + , TE.chainFirst(({tokenProvider,tokenProviderADABalance,tokenAsset,tokenProviderMintedTokenBalance}) => TE.of(pipe( + log(`Token Provider (${tokenProvider.address})`), + () => log( ` - ${ADA.format(tokenProviderADABalance)}`), + () => log( ` - ${tokenProviderMintedTokenBalance} ${tokenAsset.toString()}`)))) + , TE.chainFirst(({tokenProviderMintedTokenBalance}) => TE.of(expect(tokenProviderMintedTokenBalance).toBe(scheme.tokenprovider.tokenAmount))) + , TE.map (({adaProvider,tokenProvider,tokenAsset}) => + ({ adaProvider:adaProvider + , tokenProvider:tokenProvider + , tokenAsset:token(tokenAsset.policyId,tokenAsset.tokenName) + , initialise : (wallet : SingleAddressWallet ) => + initialise + (AxiosRestClient(runtimeURL)) + (wallet.waitConfirmation) + (wallet.signMarloweTx) + ({ changeAddress: wallet.address + , usedAddresses: O.none + , collateralUTxOs: O.none}) + , applyInputs : (wallet : SingleAddressWallet ) => + applyInputs + (AxiosRestClient(runtimeURL)) + (wallet.waitConfirmation) + (wallet.signMarloweTx) + ({ changeAddress: wallet.address + , usedAddresses: O.none + , collateralUTxOs: O.none}) + , withdraw : (wallet : SingleAddressWallet ) => + withdraw + (AxiosRestClient(runtimeURL)) + (wallet.waitConfirmation) + (wallet.signMarloweTx) + ({ changeAddress: wallet.address + , usedAddresses: O.none + , collateralUTxOs: O.none}) }))) + + +export const initialiseBankAndverifyProvisionning + = (runtimeURL: string) => + (walletContext: Context) => + (bankPrivateKey : PrivateKeysAsHex) => + pipe( TE.Do + , T.bind('bank',() => SingleAddressWallet.Initialise (walletContext,bankPrivateKey)) + , TE.fromTask + // Check Banks treasury + , TE.bind('bankBalance',({bank}) => bank.adaBalance) + , TE.chainFirst(({bankBalance,bank}) => TE.of(pipe( + log(`Bank (${bank.address})`), + () => log(` - ${ADA.format(bankBalance)}`)))) + , TE.chainFirst(({bankBalance}) => TE.of(expect(bankBalance).toBeGreaterThan(100_000_000))) + , TE.map (({bank}) => + ({ bank : bank + , restApi : AxiosRestClient(runtimeURL) + , initialise:initialise + (AxiosRestClient(runtimeURL)) + (bank.waitConfirmation) + (bank.signMarloweTx) + ({ changeAddress: bank.address + , usedAddresses: O.none + , collateralUTxOs: O.none}) + , applyInputs :applyInputs + (AxiosRestClient(runtimeURL)) + (bank.waitConfirmation) + (bank.signMarloweTx) + ({ changeAddress: bank.address + , usedAddresses: O.none + , collateralUTxOs: O.none})}))) \ No newline at end of file diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 00000000..b7c759b2 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "composite": false, + "noEmit": true + }, + "include": ["./global.d.ts"], +} diff --git a/tsconfig-base.json b/tsconfig-base.json new file mode 100644 index 00000000..47be75b1 --- /dev/null +++ b/tsconfig-base.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "allowJs": true, + "allowSyntheticDefaultImports": true, + "baseUrl": "./", + "paths": + { "@adapter/*": ["src/adapter/*"] + , "@runtime/*": ["src/runtime/*"] + , "@language/*": ["src/language/*"] + }, + "declaration": true, + "esModuleInterop": true, + "inlineSourceMap": false, + "lib": ["es2020"], + "listEmittedFiles": false, + "listFiles": false, + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "pretty": true, + "resolveJsonModule": true, + "rootDir": "src", + "skipLibCheck": true, + "strict": true, + "traceResolution": false, + "strictPropertyInitialization":false, + "types": ["node", "jest"] + }, + "compileOnSave": false, + "exclude": ["node_modules", "dist"], + "include": ["src"] +} diff --git a/tsconfig-cjs.json b/tsconfig-cjs.json new file mode 100644 index 00000000..fa777aa8 --- /dev/null +++ b/tsconfig-cjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig-base.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "dist/cjs", + "target": "es2020" + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 5ec666fb..72e78ca8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,104 +1,9 @@ { + "extends": "./tsconfig-base.json", "compilerOptions": { - "esModuleInterop": true, - /* 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": "ES2015", /* 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": "ES2020", /* 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": "./lib", /* 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. */ + "composite": true, + "module": "es2020", + "outDir": "dist/mjs", + "target": "es2020", } }