From 2756164ba328d211c02ffa60a0f2c8b0a0047fe2 Mon Sep 17 00:00:00 2001 From: nhenin Date: Wed, 3 Jan 2024 09:21:03 +0100 Subject: [PATCH] removed SingleAddressWallet for a proper Lucid implementation --- .vscode/launch.json | 12 +- doc/howToDevelop.md | 136 ++++++- jest.e2e.config.js | 7 + jest.config.js => jest.unit.config.js | 2 +- jsdelivr-npm-importmap.js | 121 +++---- package-lock.json | 46 ++- package.json | 6 +- packages/adapter/package.json | 8 + packages/adapter/src/lucid.ts | 21 ++ packages/adapter/src/time.ts | 36 +- packages/runtime/client/rest/package.json | 2 +- packages/runtime/client/rest/src/guards.ts | 6 +- packages/runtime/client/rest/src/index.ts | 7 + .../runtime/client/rest/src/runtime/index.ts | 1 + .../runtime/client/rest/src/runtime/status.ts | 13 +- .../client/rest/src/runtime/version.ts | 24 ++ packages/runtime/client/rest/test/context.ts | 6 - .../rest/test/endpoints/contracts.spec.e2e.ts | 18 +- .../rest/test/endpoints/payouts.spec.e2e.ts | 12 +- .../rest/test/endpoints/runtime.spec.e2e.ts | 18 - .../test/endpoints/transactions.spec.e2e.ts | 17 - .../test/endpoints/withdrawals.spec.e2e.ts | 12 - .../client/rest/test/jest.e2e.config.mjs | 35 +- packages/runtime/core/package.json | 3 + packages/runtime/lifecycle/package.json | 5 +- .../runtime/lifecycle/src/nodejs/index.ts | 25 +- packages/runtime/lifecycle/test/context.ts | 42 --- .../test/examples/swap.ada.token.e2e.spec.ts | 177 ++++----- .../test/generic/contracts.e2e.spec.ts | 91 +++-- .../test/generic/payouts.e2e.spec.ts | 163 ++++----- .../lifecycle/test/jest.e2e.config.mjs | 38 +- .../runtime/lifecycle/test/provisionning.ts | 116 ------ packages/testing-kit/.npmignore | 1 + packages/testing-kit/Readme.md | 3 + packages/testing-kit/package.json | 44 +++ .../src/environment/configuration.ts | 116 ++++++ packages/testing-kit/src/environment/index.ts | 100 ++++++ .../src/executable/generateSeedPhrase.ts | 18 + packages/testing-kit/src/index.ts | 5 + packages/testing-kit/src/logging.ts | 34 ++ packages/testing-kit/src/tsconfig.json | 18 + packages/testing-kit/src/wallet/api.ts | 80 +++++ .../testing-kit/src/wallet/lucid/index.ts | 80 +++++ .../src/wallet/lucid/provisionning.ts | 181 ++++++++++ packages/testing-kit/src/wallet/seedPhrase.ts | 29 ++ packages/testing-kit/typedoc.json | 5 + packages/wallet/package.json | 5 - packages/wallet/src/lucid/index.ts | 16 +- packages/wallet/src/nodejs/index.ts | 340 ------------------ packages/wallet/test/wallet.spec.ts | 3 +- tsconfig-base.json | 2 +- tsconfig.json | 3 +- 52 files changed, 1370 insertions(+), 939 deletions(-) create mode 100644 jest.e2e.config.js rename jest.config.js => jest.unit.config.js (77%) create mode 100644 packages/adapter/src/lucid.ts create mode 100644 packages/runtime/client/rest/src/runtime/version.ts delete mode 100644 packages/runtime/client/rest/test/context.ts delete mode 100644 packages/runtime/client/rest/test/endpoints/runtime.spec.e2e.ts delete mode 100644 packages/runtime/client/rest/test/endpoints/transactions.spec.e2e.ts delete mode 100644 packages/runtime/client/rest/test/endpoints/withdrawals.spec.e2e.ts delete mode 100644 packages/runtime/lifecycle/test/context.ts delete mode 100644 packages/runtime/lifecycle/test/provisionning.ts create mode 100644 packages/testing-kit/.npmignore create mode 100644 packages/testing-kit/Readme.md create mode 100644 packages/testing-kit/package.json create mode 100644 packages/testing-kit/src/environment/configuration.ts create mode 100644 packages/testing-kit/src/environment/index.ts create mode 100644 packages/testing-kit/src/executable/generateSeedPhrase.ts create mode 100644 packages/testing-kit/src/index.ts create mode 100644 packages/testing-kit/src/logging.ts create mode 100644 packages/testing-kit/src/tsconfig.json create mode 100644 packages/testing-kit/src/wallet/api.ts create mode 100644 packages/testing-kit/src/wallet/lucid/index.ts create mode 100644 packages/testing-kit/src/wallet/lucid/provisionning.ts create mode 100644 packages/testing-kit/src/wallet/seedPhrase.ts create mode 100644 packages/testing-kit/typedoc.json delete mode 100644 packages/wallet/src/nodejs/index.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index c6f80079..2d5b262a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,10 +4,18 @@ { "type": "node", "request": "launch", - "name": "Debug Tests", + "name": "Debug Unit Tests", "program": "${workspaceRoot}/node_modules/.bin/jest", "cwd": "${workspaceRoot}", - "args": ["--i", "--config", "jest.config.js"], + "args": ["--i", "--config", "jest.unit.config.js"], + }, + { + "type": "node", + "request": "launch", + "name": "Debug E2E Tests", + "program": "${workspaceRoot}/node_modules/.bin/jest", + "cwd": "${workspaceRoot}", + "args": ["--i", "--config", "jest.e2e.config.js"], }, { "name": "Launch Extension (development)", diff --git a/doc/howToDevelop.md b/doc/howToDevelop.md index c75ebca6..141638c7 100644 --- a/doc/howToDevelop.md +++ b/doc/howToDevelop.md @@ -1,17 +1,17 @@ # Development -## Build +# Build In order to start develop the SDK you need to install the dependencies and build the packages. -``` +```bash $ npm i $ npm run build ``` If you want to build a single package you can use the `-w` flag or execute the build command from the package folder. -``` +```bash # From the root folder $ npm run build -w @marlowe.io/language-core-v1 # Or you can enter the package and build @@ -19,35 +19,135 @@ $ cd packages/language/core/v1 $ npm run build ``` +# Clean + In order to clean the build artifacts you can use the `clean` command. -``` +```bash $ npm run clean ``` -To run the unit test you can execute the `test` command. +# Tests +N.B : It is recommended to clean and build the packages before you run the tests to be sure you are playing with the most up to date version of the codebase. + +```bash +$ npm run clean && npm ``` + +## Unit Tests + +To run the unit tests for all the packages, from the root folder you can execute the `test` command : + +```bash +$ npm run test +``` + +If you want to run tests for a single package you can use the `-w` flag or execute the build command from the package folder. + +```bash +# From the root folder +$ npm run clean && npm run build && npm run test -w @marlowe.io/language-core-v1 +# Or you can enter the package folder and test. You will have to clean and build properly the local package +# dependencies of this current package if you modify one of them +# e.g : `packages/language/core/v1` depends on `packages/adapter`. Be sure you have build correctly this package before runnning your test that way. +$ cd packages/language/core/v1 $ npm run test ``` -## E2E tests +## Integration/E2E Tests + +### Setting up the env Configuration File + +1. Create a `./env/.env.test` at the root of the project +2. Copy/Paste the following, and provide the necessary parameter + +```bash +#################################################### +## Provide a Runtime Instance URL (>= v0.0.5) # +#################################################### +## to create an instance of a local Marlowe runtime, follow the instructions in +## the Marlowe starter kit : https://github.com/input-output-hk/ marlowe-starter-kit/blob/main/docs/preliminaries.md +MARLOWE_WEB_SERVER_URL="http://:" +#################################################### + +##################################################### +## Provide Wallet Dependencies (Necessary for Lucid Library) +##################################################### +## Blockfrost Account : If you haven't done it before, go to https://blockfrost.io/ and create a free-tier account. +## Then, create a project and copy the project ID +BLOCKFROST_PROJECT_ID="" +BLOCKFROST_URL="" +## Network used by Blockfrost : private | preview | preprod | mainnet +NETWORK_NAME=preprod +## Bank Seed Phrase : The bank is a wallet where you provision enough tAda (>= 100 tAda) to run all +## the e2e tests without running out of money. This is your responsability to create this wallet and +## add tAda using a Faucet. +BANK_SEED_PHRASE='[ + "deal", + "place", + "depart", + "sound", + "kick", + "daughter", + "diamond", + "rebel", + "update", + "shoe", + "benefit", + "useful", + "travel", + "fringe", + "culture", + "dog", + "lawsuit", + "combine", + "run", + "vanish", + "warm", + "rubber", + "quit", + "system" +]' +##################################################### + +##################################################### +## Logging +##################################################### +## set to true or false if you want to log Debug Info +LOG_DEBUG_LEVEL=false +``` +#### How to Generate a new Seed Phrase for a Bank Wallet ? + +1. At the root of the project : +```bash +npm run -w @marlowe.io/testing-kit genSeedPhrase +``` +2. Copy/paste the words within quotes in the env file. -In order to run the E2E tests you need to create a `./env/.env.test` file that points to a working version of the Marlowe runtime and a working Blockfrost instance and a faucet PK. +#### How to add tAda to the Bank Wallet via a faucet ? -If you haven't done it before, go to https://blockfrost.io/ and create a free-tier account. Then, create a project and copy the project ID. Blockfrost is a Lucid dependency, eventually when -we migrate to a different library this wont be necessary. +1. Retrieve your Bank Wallet payment address +2. Go to https://docs.cardano.org/cardano-testnet/tools/faucet ask for test Ada on this address. +3. Wait a moment till the transaction is confirmed and you should be able to run the tests. -To create an instance of a local Marlowe runtime, follow the instructions in the [Marlowe starter kit](https://github.com/input-output-hk/marlowe-starter-kit/blob/main/docs/preliminaries.md) +### Running the E2E Tests -TODO: explain how to get the Faucet PK +To run the e2e tests for all the packages, from the root folder you can execute the `test:e2e` command : +```bash +$ npm run test:e2e ``` -MARLOWE_WEB_SERVER_URL="http://:33294/" -BLOCKFROST_PROJECT_ID="" -BLOCKFROST_URL="https://cardano-preprod.blockfrost.io/api/v0" -NETWORK_ID=Preprod -BANK_PK_HEX='' + +If you want to run tests for a single package you can use the `-w` flag or execute the build command from the package folder. + +```bash +# From the root folder +$ npm run clean && npm run build && npm run test:e2e -w @marlowe.io/runtime-lifecycle +# Or you can enter the package folder and test. You will have to clean and build properly the local package +# dependencies of this current package if you modify one of them +$ cd packages/runtime/client/rest +$ npm run test:e2e ``` ## Documentation @@ -56,7 +156,7 @@ BANK_PK_HEX='' To compile all documentation -``` +```bash $ npm run docs ``` @@ -89,7 +189,7 @@ This project manages its changelog with [scriv](https://github.com/nedbat/scriv) Create a new changelog entry template with -``` +```s $ scriv create ``` diff --git a/jest.e2e.config.js b/jest.e2e.config.js new file mode 100644 index 00000000..d050666d --- /dev/null +++ b/jest.e2e.config.js @@ -0,0 +1,7 @@ +module.exports = { + testEnvironment: "node", + projects: [ + "/packages/runtime/client/rest/test/jest.e2e.config.mjs", + "/packages/runtime/lifecycle/test/jest.e2e.config.mjs", + ], +}; diff --git a/jest.config.js b/jest.unit.config.js similarity index 77% rename from jest.config.js rename to jest.unit.config.js index 60e4253a..6ea6942a 100644 --- a/jest.config.js +++ b/jest.unit.config.js @@ -3,6 +3,6 @@ module.exports = { projects: [ "/packages/language/core/v1/test/jest.unit.config.mjs", "/packages/language/examples/test/jest.unit.config.mjs", - "/packages/wallet/test/jest.unit.config.mjs", + "/packages/wallet/test/jest.unit.config.mjs" ], }; diff --git a/jsdelivr-npm-importmap.js b/jsdelivr-npm-importmap.js index 54b79e36..67e22ef4 100644 --- a/jsdelivr-npm-importmap.js +++ b/jsdelivr-npm-importmap.js @@ -1,81 +1,46 @@ const importMap = { - imports: { - "@marlowe.io/adapter": - "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/adapter.js", - "@marlowe.io/adapter/assoc-map": - "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/assoc-map.js", - "@marlowe.io/adapter/codec": - "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/codec.js", - "@marlowe.io/adapter/deep-equal": - "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/deep-equal.js", - "@marlowe.io/adapter/file": - "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/file.js", - "@marlowe.io/adapter/fp-ts": - "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/fp-ts.js", - "@marlowe.io/adapter/http": - "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/http.js", - "@marlowe.io/adapter/io-ts": - "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/io-ts.js", - "@marlowe.io/adapter/time": - "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/time.js", - "@marlowe.io/language-core-v1": - "https://cdn.jsdelivr.net/npm/@marlowe.io/language-core-v1@0.3.0-beta-rc1/dist/bundled/esm/language-core-v1.js", - "@marlowe.io/language-core-v1/guards": - "https://cdn.jsdelivr.net/npm/@marlowe.io/language-core-v1@0.3.0-beta-rc1/dist/bundled/esm/guards.js", - "@marlowe.io/language-core-v1/next": - "https://cdn.jsdelivr.net/npm/@marlowe.io/language-core-v1@0.3.0-beta-rc1/dist/bundled/esm/next.js", - "@marlowe.io/language-core-v1/playground-v1": - "https://cdn.jsdelivr.net/npm/@marlowe.io/language-core-v1@0.3.0-beta-rc1/dist/bundled/esm/playground-v1.js", - "@marlowe.io/language-core-v1/semantics": - "https://cdn.jsdelivr.net/npm/@marlowe.io/language-core-v1@0.3.0-beta-rc1/dist/bundled/esm/semantics.js", - "@marlowe.io/language-core-v1/version": - "https://cdn.jsdelivr.net/npm/@marlowe.io/language-core-v1@0.3.0-beta-rc1/dist/bundled/esm/version.js", - "@marlowe.io/language-examples": - "https://cdn.jsdelivr.net/npm/@marlowe.io/language-examples@0.3.0-beta-rc1/dist/bundled/esm/language-examples.js", - "@marlowe.io/language-specification-client": - "https://cdn.jsdelivr.net/npm/@marlowe.io/language-specification-client@0.3.0-beta-rc1/dist/bundled/esm/language-specification-client.js", - "@marlowe.io/token-metadata-client": - "https://cdn.jsdelivr.net/npm/@marlowe.io/token-metadata-client@0.3.0-beta-rc1/dist/bundled/esm/token-metadata-client.js", - "@marlowe.io/wallet": - "https://cdn.jsdelivr.net/npm/@marlowe.io/wallet@0.3.0-beta-rc1/dist/bundled/esm/wallet.js", - "@marlowe.io/wallet/api": - "https://cdn.jsdelivr.net/npm/@marlowe.io/wallet@0.3.0-beta-rc1/dist/bundled/esm/api.js", - "@marlowe.io/wallet/browser": - "https://cdn.jsdelivr.net/npm/@marlowe.io/wallet@0.3.0-beta-rc1/dist/bundled/esm/browser.js", - "@marlowe.io/wallet/lucid": - "https://cdn.jsdelivr.net/npm/@marlowe.io/wallet@0.3.0-beta-rc1/dist/bundled/esm/lucid.js", - "@marlowe.io/wallet/nodejs": - "https://cdn.jsdelivr.net/npm/@marlowe.io/wallet@0.3.0-beta-rc1/dist/bundled/esm/nodejs.js", - "@marlowe.io/runtime-rest-client": - "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-rest-client@0.3.0-beta-rc1/dist/bundled/esm/runtime-rest-client.js", - "@marlowe.io/runtime-rest-client/contract": - "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-rest-client@0.3.0-beta-rc1/dist/bundled/esm/contract.js", - "@marlowe.io/runtime-rest-client/guards": - "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-rest-client@0.3.0-beta-rc1/dist/bundled/esm/guards.js", - "@marlowe.io/runtime-rest-client/payout": - "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-rest-client@0.3.0-beta-rc1/dist/bundled/esm/payout.js", - "@marlowe.io/runtime-rest-client/transaction": - "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-rest-client@0.3.0-beta-rc1/dist/bundled/esm/transaction.js", - "@marlowe.io/runtime-rest-client/withdrawal": - "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-rest-client@0.3.0-beta-rc1/dist/bundled/esm/withdrawal.js", - "@marlowe.io/runtime-core": - "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-core@0.3.0-beta-rc1/dist/bundled/esm/runtime-core.js", - "@marlowe.io/runtime-lifecycle": - "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-lifecycle@0.3.0-beta-rc1/dist/bundled/esm/runtime-lifecycle.js", - "@marlowe.io/runtime-lifecycle/api": - "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-lifecycle@0.3.0-beta-rc1/dist/bundled/esm/api.js", - "@marlowe.io/runtime-lifecycle/browser": - "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-lifecycle@0.3.0-beta-rc1/dist/bundled/esm/browser.js", - "@marlowe.io/runtime-lifecycle/generic": - "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-lifecycle@0.3.0-beta-rc1/dist/bundled/esm/generic.js", - "@marlowe.io/marlowe-object": - "https://cdn.jsdelivr.net/npm/@marlowe.io/marlowe-object@0.3.0-beta-rc1/dist/bundled/esm/marlowe-object.js", - "@marlowe.io/marlowe-object/guards": - "https://cdn.jsdelivr.net/npm/@marlowe.io/marlowe-object@0.3.0-beta-rc1/dist/bundled/esm/guards.js", - "lucid-cardano": "https://unpkg.com/lucid-cardano@0.10.7/web/mod.js", - }, + "imports": { + "@marlowe.io/adapter": "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/adapter.js", + "@marlowe.io/adapter/assoc-map": "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/assoc-map.js", + "@marlowe.io/adapter/codec": "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/codec.js", + "@marlowe.io/adapter/deep-equal": "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/deep-equal.js", + "@marlowe.io/adapter/file": "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/file.js", + "@marlowe.io/adapter/fp-ts": "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/fp-ts.js", + "@marlowe.io/adapter/http": "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/http.js", + "@marlowe.io/adapter/io-ts": "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/io-ts.js", + "@marlowe.io/adapter/lucid": "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/lucid.js", + "@marlowe.io/adapter/time": "https://cdn.jsdelivr.net/npm/@marlowe.io/adapter@0.3.0-beta-rc1/dist/bundled/esm/time.js", + "@marlowe.io/language-core-v1": "https://cdn.jsdelivr.net/npm/@marlowe.io/language-core-v1@0.3.0-beta-rc1/dist/bundled/esm/language-core-v1.js", + "@marlowe.io/language-core-v1/guards": "https://cdn.jsdelivr.net/npm/@marlowe.io/language-core-v1@0.3.0-beta-rc1/dist/bundled/esm/guards.js", + "@marlowe.io/language-core-v1/next": "https://cdn.jsdelivr.net/npm/@marlowe.io/language-core-v1@0.3.0-beta-rc1/dist/bundled/esm/next.js", + "@marlowe.io/language-core-v1/playground-v1": "https://cdn.jsdelivr.net/npm/@marlowe.io/language-core-v1@0.3.0-beta-rc1/dist/bundled/esm/playground-v1.js", + "@marlowe.io/language-core-v1/semantics": "https://cdn.jsdelivr.net/npm/@marlowe.io/language-core-v1@0.3.0-beta-rc1/dist/bundled/esm/semantics.js", + "@marlowe.io/language-core-v1/version": "https://cdn.jsdelivr.net/npm/@marlowe.io/language-core-v1@0.3.0-beta-rc1/dist/bundled/esm/version.js", + "@marlowe.io/language-examples": "https://cdn.jsdelivr.net/npm/@marlowe.io/language-examples@0.3.0-beta-rc1/dist/bundled/esm/language-examples.js", + "@marlowe.io/language-specification-client": "https://cdn.jsdelivr.net/npm/@marlowe.io/language-specification-client@0.3.0-beta-rc1/dist/bundled/esm/language-specification-client.js", + "@marlowe.io/token-metadata-client": "https://cdn.jsdelivr.net/npm/@marlowe.io/token-metadata-client@0.3.0-beta-rc1/dist/bundled/esm/token-metadata-client.js", + "@marlowe.io/wallet": "https://cdn.jsdelivr.net/npm/@marlowe.io/wallet@0.3.0-beta-rc1/dist/bundled/esm/wallet.js", + "@marlowe.io/wallet/api": "https://cdn.jsdelivr.net/npm/@marlowe.io/wallet@0.3.0-beta-rc1/dist/bundled/esm/api.js", + "@marlowe.io/wallet/browser": "https://cdn.jsdelivr.net/npm/@marlowe.io/wallet@0.3.0-beta-rc1/dist/bundled/esm/browser.js", + "@marlowe.io/wallet/lucid": "https://cdn.jsdelivr.net/npm/@marlowe.io/wallet@0.3.0-beta-rc1/dist/bundled/esm/lucid.js", + "@marlowe.io/runtime-rest-client": "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-rest-client@0.3.0-beta-rc1/dist/bundled/esm/runtime-rest-client.js", + "@marlowe.io/runtime-rest-client/contract": "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-rest-client@0.3.0-beta-rc1/dist/bundled/esm/contract.js", + "@marlowe.io/runtime-rest-client/guards": "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-rest-client@0.3.0-beta-rc1/dist/bundled/esm/guards.js", + "@marlowe.io/runtime-rest-client/payout": "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-rest-client@0.3.0-beta-rc1/dist/bundled/esm/payout.js", + "@marlowe.io/runtime-rest-client/transaction": "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-rest-client@0.3.0-beta-rc1/dist/bundled/esm/transaction.js", + "@marlowe.io/runtime-rest-client/withdrawal": "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-rest-client@0.3.0-beta-rc1/dist/bundled/esm/withdrawal.js", + "@marlowe.io/runtime-core": "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-core@0.3.0-beta-rc1/dist/bundled/esm/runtime-core.js", + "@marlowe.io/runtime-lifecycle": "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-lifecycle@0.3.0-beta-rc1/dist/bundled/esm/runtime-lifecycle.js", + "@marlowe.io/runtime-lifecycle/api": "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-lifecycle@0.3.0-beta-rc1/dist/bundled/esm/api.js", + "@marlowe.io/runtime-lifecycle/browser": "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-lifecycle@0.3.0-beta-rc1/dist/bundled/esm/browser.js", + "@marlowe.io/runtime-lifecycle/generic": "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-lifecycle@0.3.0-beta-rc1/dist/bundled/esm/generic.js", + "@marlowe.io/marlowe-object": "https://cdn.jsdelivr.net/npm/@marlowe.io/marlowe-object@0.3.0-beta-rc1/dist/bundled/esm/marlowe-object.js", + "@marlowe.io/marlowe-object/guards": "https://cdn.jsdelivr.net/npm/@marlowe.io/marlowe-object@0.3.0-beta-rc1/dist/bundled/esm/guards.js", + "@marlowe.io/testing-kit": "https://cdn.jsdelivr.net/npm/@marlowe.io/testing-kit@0.3.0-beta-rc1/dist/bundled/esm/testing-kit.js", + "lucid-cardano": "https://unpkg.com/lucid-cardano@0.10.7/web/mod.js" + } }; -const im = document.createElement("script"); -im.type = "importmap"; +const im = document.createElement('script'); +im.type = 'importmap'; im.textContent = JSON.stringify(importMap); -document.currentScript.after(im); +document.currentScript.after(im); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b5d494be..37f98c9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "packages/runtime/client/rest", "packages/runtime/core", "packages/runtime/lifecycle", - "packages/marlowe-object" + "packages/marlowe-object", + "packages/testing-kit" ], "devDependencies": { "@blockfrost/blockfrost-js": "5.2.0", @@ -1430,6 +1431,10 @@ "resolved": "packages/runtime/client/rest", "link": true }, + "node_modules/@marlowe.io/testing-kit": { + "resolved": "packages/testing-kit", + "link": true + }, "node_modules/@marlowe.io/token-metadata-client": { "resolved": "packages/token-metadata-client", "link": true @@ -1438,6 +1443,17 @@ "resolved": "packages/wallet", "link": true }, + "node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -2430,6 +2446,14 @@ "node": ">=8" } }, + "node_modules/bip39": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", + "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", + "dependencies": { + "@noble/hashes": "^1.2.0" + } + }, "node_modules/blake2b": { "version": "2.1.3", "license": "ISC", @@ -9882,6 +9906,9 @@ "fp-ts": "^2.16.1", "io-ts": "2.2.21", "newtype-ts": "0.3.5" + }, + "devDependencies": { + "@marlowe.io/testing-kit": "0.3.0-beta-rc1" } }, "packages/runtime/lifecycle": { @@ -9898,6 +9925,23 @@ "io-ts": "2.2.21", "monocle-ts": "2.3.13", "newtype-ts": "0.3.5" + }, + "devDependencies": { + "@marlowe.io/testing-kit": "0.3.0-beta-rc1" + } + }, + "packages/testing-kit": { + "name": "@marlowe.io/testing-kit", + "version": "0.3.0-beta-rc1", + "license": "Apache-2.0", + "dependencies": { + "@marlowe.io/adapter": "0.3.0-beta-rc1", + "@marlowe.io/language-core-v1": "0.3.0-beta-rc1", + "@marlowe.io/runtime-core": "0.3.0-beta-rc1", + "@marlowe.io/runtime-rest-client": "0.3.0-beta-rc1", + "@marlowe.io/wallet": "0.3.0-beta-rc1", + "bip39": "3.1.0", + "fp-ts": "2.16.1" } }, "packages/token-metadata-client": { diff --git a/package.json b/package.json index 63479741..7c3578f0 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "scripts": { "build": "tsc --version && tsc --build && shx mkdir -p dist && rollup --config rollup/config.mjs", "clean": "npm run clean --workspaces && shx rm -rf dist", - "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest", + "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --config ./jest.unit.config.js --verbose", + "test:e2e": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --runInBand --config ./jest.e2e.config.js --verbose", "docs": "typedoc . --treatWarningsAsErrors --options ./typedoc.json", "serve": "ws --port 1337 --rewrite '/importmap -> https://cdn.jsdelivr.net/gh/input-output-hk/marlowe-ts-sdk@0.2.0-beta/jsdelivr-npm-importmap.js'", "serve-dev": "ws --port 1337 --rewrite '/importmap -> /dist/local-importmap.js'" @@ -33,7 +34,8 @@ "packages/runtime/client/rest", "packages/runtime/core", "packages/runtime/lifecycle", - "packages/marlowe-object" + "packages/marlowe-object", + "packages/testing-kit" ], "devDependencies": { "@blockfrost/blockfrost-js": "5.2.0", diff --git a/packages/adapter/package.json b/packages/adapter/package.json index 95acf1c3..5e32fc40 100644 --- a/packages/adapter/package.json +++ b/packages/adapter/package.json @@ -18,6 +18,9 @@ "test": "echo 'adapter doesnt have tests for the moment'" }, "type": "module", + "module": "./dist/esm/index.js", + "main": "./dist/esm/index.js", + "types": "./dist/esm/index.d.ts", "files": [ "dist" ], @@ -62,6 +65,11 @@ "require": "./dist/bundled/cjs/assoc-map.cjs", "types": "./dist/esm/assoc-map.d.ts" }, + "./lucid": { + "import": "./dist/esm/lucid.js", + "require": "./dist/bundled/cjs/lucid.cjs", + "types": "./dist/esm/lucid.d.ts" + }, "./deep-equal": { "import": "./dist/esm/deep-equal.js", "require": "./dist/bundled/cjs/deep-equal.cjs", diff --git a/packages/adapter/src/lucid.ts b/packages/adapter/src/lucid.ts new file mode 100644 index 00000000..f66e6810 --- /dev/null +++ b/packages/adapter/src/lucid.ts @@ -0,0 +1,21 @@ +import { Monoid } from "fp-ts/lib/Monoid.js"; + +import * as R from "fp-ts/lib/Record.js"; + +export type LucidAssets = { + [x: string]: bigint; +}; + +// Function that tells how to join two assets +const addAssets = { concat: (x: bigint, y: bigint) => x + y }; + +// A monoid for Lucid's Assets indicates how to create +// an empty Assets object and how to merge two Assets objects. +export const mergeAssets: Monoid = { + // Lucid's Assets object are a Record, + // so the empty assets is the empty object + empty: {}, + // And to join two assets we join the two records. When + // the "assetId" is the same, the quantities are added. + concat: (x, y) => R.union(addAssets)(x)(y), +}; diff --git a/packages/adapter/src/time.ts b/packages/adapter/src/time.ts index 08c64541..06e991e6 100644 --- a/packages/adapter/src/time.ts +++ b/packages/adapter/src/time.ts @@ -1,6 +1,4 @@ import * as t from "io-ts/lib/index.js"; -import { pipe } from "fp-ts/lib/function.js"; -import { format, formatISO } from "date-fns"; export type ISO8601 = t.TypeOf; export const ISO8601 = t.string; @@ -14,3 +12,37 @@ export const datetoIso8601 = (date: Date): ISO8601 => date.toISOString(); // a minute in milliseconds export const MINUTES = 1000 * 60; + +/** + * Block the execution flow till a promise Predicate becomes true. + * @param predicate + * @param interval at which the predicate is re-evaluated + * @returns + */ +export const waitForPredicatePromise = async ( + predicate: () => Promise, + interval: number = 3_000 +): Promise => { + if (await predicate()) { + // Predicate is already true, no need to wait + return; + } + // Use a promise to wait for the specified interval + const wait = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + + // Wait for the specified interval + await wait(interval); + + // Recursive call to continue checking the predicate + await waitForPredicatePromise(predicate, interval); +} + +/** + * Block the execution flow for a given number of seconds + * @param secondes + * @returns + */ +export const sleep = (secondes: number): Promise => { + return new Promise((resolve) => setTimeout(resolve, secondes * 1_000)); +} \ No newline at end of file diff --git a/packages/runtime/client/rest/package.json b/packages/runtime/client/rest/package.json index 1226b358..ee4f33a3 100644 --- a/packages/runtime/client/rest/package.json +++ b/packages/runtime/client/rest/package.json @@ -16,7 +16,7 @@ "build": "tsc --build src", "clean": "shx rm -rf dist", "test": "echo 'The client rest doesnt have unit tests'", - "test:e2e": "npm run clean && npm run build && NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --runInBand --config test/jest.e2e.config.mjs --verbose" + "test:e2e": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --runInBand --config test/jest.e2e.config.mjs --verbose" }, "type": "module", "module": "./dist/esm/index.js", diff --git a/packages/runtime/client/rest/src/guards.ts b/packages/runtime/client/rest/src/guards.ts index eb92f510..bf98b4e8 100644 --- a/packages/runtime/client/rest/src/guards.ts +++ b/packages/runtime/client/rest/src/guards.ts @@ -27,7 +27,9 @@ export { export { ContractDetailsGuard as ContractDetails } from "./contract/details.js"; +export { TipGuard as Tip } from "./runtime/status.js"; + export { + RuntimeVersionGuard as RuntimeVersion, CompatibleRuntimeVersionGuard as CompatibleRuntimeVersion, - TipGuard as Tip, -} from "./runtime/status.js"; +} from "./runtime/version.js"; diff --git a/packages/runtime/client/rest/src/index.ts b/packages/runtime/client/rest/src/index.ts index cb8644fd..d9e68522 100644 --- a/packages/runtime/client/rest/src/index.ts +++ b/packages/runtime/client/rest/src/index.ts @@ -51,6 +51,13 @@ export { PageGuard, } from "./pagination.js"; +export { + RuntimeStatus, + RuntimeVersion, + Tip, + CompatibleRuntimeVersion, +} from "./runtime/index.js"; + /** * The RestClient offers a simple abstraction for the {@link https://docs.marlowe.iohk.io/api/ | Marlowe Runtime REST API} endpoints. * You can create an instance of the RestClient using the {@link mkRestClient} function. diff --git a/packages/runtime/client/rest/src/runtime/index.ts b/packages/runtime/client/rest/src/runtime/index.ts index c9e56c5e..76d1eb11 100644 --- a/packages/runtime/client/rest/src/runtime/index.ts +++ b/packages/runtime/client/rest/src/runtime/index.ts @@ -1 +1,2 @@ export * from "./status.js"; +export * from "./version.js"; diff --git a/packages/runtime/client/rest/src/runtime/status.ts b/packages/runtime/client/rest/src/runtime/status.ts index 0271462c..19288b91 100644 --- a/packages/runtime/client/rest/src/runtime/status.ts +++ b/packages/runtime/client/rest/src/runtime/status.ts @@ -12,16 +12,7 @@ import { formatValidationErrors } from "jsonbigint-io-ts-reporters"; import * as E from "fp-ts/lib/Either.js"; import * as t from "io-ts/lib/index.js"; import { MarloweJSON, MarloweJSONCodec } from "@marlowe.io/adapter/codec"; -export type BlockHash = string; - -export type RuntimeVersion = string; - -export type CompatibleRuntimeVersion = "0.0.6" | "0.0.5"; - -export const CompatibleRuntimeVersionGuard: t.Type< - CompatibleRuntimeVersion, - string -> = t.union([t.literal("0.0.6"), t.literal("0.0.5")]); +import { RuntimeVersion } from "./version.js"; /** * A **Tip** represents the last block read in a "projection" process. @@ -60,8 +51,6 @@ export type RuntimeStatus = { version: RuntimeVersion; /** * Set of Tips providing information on how healthy is the flow of Projections : Node > Runtime Chain > Runtime - * The Runtime Tip indicates if the information Queried is up to date. The Node and the Runtime Chain Tips are - * here to help the diagnostic of a Runtime Tip that would be too long in the past or not being updated anymore. */ tips: { node: Tip; diff --git a/packages/runtime/client/rest/src/runtime/version.ts b/packages/runtime/client/rest/src/runtime/version.ts new file mode 100644 index 00000000..5bcf65dc --- /dev/null +++ b/packages/runtime/client/rest/src/runtime/version.ts @@ -0,0 +1,24 @@ +import * as t from "io-ts/lib/index.js"; +import { unsafeEither } from "@marlowe.io/adapter/fp-ts"; + +export interface RuntimeVersionBrand { + readonly RuntimeVersion: unique symbol; +} + +export const RuntimeVersionGuard = t.brand( + t.string, + (s): s is t.Branded => true, + "RuntimeVersion" +); + +export type RuntimeVersion = t.TypeOf; + +export const runtimeVersion = (s: string) => + unsafeEither(RuntimeVersionGuard.decode(s)); + +export type CompatibleRuntimeVersion = "0.0.6" | "0.0.5"; + +export const CompatibleRuntimeVersionGuard: t.Type< + CompatibleRuntimeVersion, + string +> = t.union([t.literal("0.0.6"), t.literal("0.0.5")]); diff --git a/packages/runtime/client/rest/test/context.ts b/packages/runtime/client/rest/test/context.ts deleted file mode 100644 index 4b658a77..00000000 --- a/packages/runtime/client/rest/test/context.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function getMarloweRuntimeUrl(): string { - const { MARLOWE_WEB_SERVER_URL } = process.env; - if (MARLOWE_WEB_SERVER_URL == undefined) - throw "environment configurations not available(MARLOWE_WEB_SERVER_URL)"; - return MARLOWE_WEB_SERVER_URL as string; -} diff --git a/packages/runtime/client/rest/test/endpoints/contracts.spec.e2e.ts b/packages/runtime/client/rest/test/endpoints/contracts.spec.e2e.ts index 954818df..64b06f7d 100644 --- a/packages/runtime/client/rest/test/endpoints/contracts.spec.e2e.ts +++ b/packages/runtime/client/rest/test/endpoints/contracts.spec.e2e.ts @@ -1,19 +1,16 @@ -import { mkRestClient } from "@marlowe.io/runtime-rest-client"; - -import { getMarloweRuntimeUrl } from "../context.js"; +import { readEnvConfigurationFile } from "@marlowe.io/testing-kit"; import console from "console"; global.console = console; describe("contracts endpoints", () => { - const restClient = mkRestClient(getMarloweRuntimeUrl()); - it( "can navigate throught some Marlowe Contracts pages" + "(GET: /contracts/)", async () => { - const firstPage = await restClient.getContracts({ + const { runtime } = await readEnvConfigurationFile(); + const firstPage = await runtime.client.getContracts({ tags: [], partyAddresses: [], partyRoles: [], @@ -23,7 +20,7 @@ describe("contracts endpoints", () => { expect(firstPage.page.next).toBeDefined(); - const secondPage = await restClient.getContracts({ + const secondPage = await runtime.client.getContracts({ range: firstPage.page.next, }); expect(secondPage.contracts.length).toBe(100); @@ -31,7 +28,7 @@ describe("contracts endpoints", () => { expect(secondPage.page.next).toBeDefined(); - const thirdPage = await restClient.getContracts({ + const thirdPage = await runtime.client.getContracts({ range: secondPage.page.next, }); @@ -45,7 +42,8 @@ describe("contracts endpoints", () => { it( "can retrieve some contract Details" + "(GET: /contracts/{contractId})", async () => { - const firstPage = await restClient.getContracts({ + const { runtime } = await readEnvConfigurationFile(); + const firstPage = await runtime.client.getContracts({ tags: [], partyAddresses: [], partyRoles: [], @@ -56,7 +54,7 @@ describe("contracts endpoints", () => { await Promise.all( firstPage.contracts.map((contract) => - restClient.getContractById(contract.contractId) + runtime.client.getContractById(contract.contractId) ) ); }, diff --git a/packages/runtime/client/rest/test/endpoints/payouts.spec.e2e.ts b/packages/runtime/client/rest/test/endpoints/payouts.spec.e2e.ts index f8fe22a1..2684e40e 100644 --- a/packages/runtime/client/rest/test/endpoints/payouts.spec.e2e.ts +++ b/packages/runtime/client/rest/test/endpoints/payouts.spec.e2e.ts @@ -1,17 +1,15 @@ import { mkRestClient } from "@marlowe.io/runtime-rest-client"; - -import { getMarloweRuntimeUrl } from "../context.js"; +import { readEnvConfigurationFile } from "@marlowe.io/testing-kit"; import console from "console"; global.console = console; describe("payouts endpoints", () => { - const restClient = mkRestClient(getMarloweRuntimeUrl()); - it( "can navigate throught payout headers" + "(GET: /payouts)", async () => { - const firstPage = await restClient.getPayouts({ + const { runtime } = await readEnvConfigurationFile(); + const firstPage = await runtime.client.getPayouts({ contractIds: [], roleTokens: [], }); @@ -20,7 +18,7 @@ describe("payouts endpoints", () => { expect(firstPage.page.next).toBeDefined(); - const secondPage = await restClient.getPayouts({ + const secondPage = await runtime.client.getPayouts({ contractIds: [], roleTokens: [], range: firstPage.page.next, @@ -30,7 +28,7 @@ describe("payouts endpoints", () => { expect(secondPage.page.next).toBeDefined(); - const thirdPage = await restClient.getPayouts({ + const thirdPage = await runtime.client.getPayouts({ contractIds: [], roleTokens: [], range: secondPage.page.next, diff --git a/packages/runtime/client/rest/test/endpoints/runtime.spec.e2e.ts b/packages/runtime/client/rest/test/endpoints/runtime.spec.e2e.ts deleted file mode 100644 index 96499cfe..00000000 --- a/packages/runtime/client/rest/test/endpoints/runtime.spec.e2e.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { mkRestClient } from "@marlowe.io/runtime-rest-client"; - -import { getMarloweRuntimeUrl } from "../context.js"; - -import console from "console"; -import * as G from "@marlowe.io/runtime-rest-client/guards"; -import { MarloweJSON } from "@marlowe.io/adapter/codec"; - -global.console = console; - -describe("Runtime", () => { - const restClient = mkRestClient(getMarloweRuntimeUrl()); - it("is deployed with a version compatible with @marlowe.io/runtime-rest-client.", async () => { - const status = await restClient.getRuntimeStatus(); - console.log("status", MarloweJSON.stringify(status)); - expect(G.CompatibleRuntimeVersion.is(status.version)).toBe(true); - }, 100_000); -}); diff --git a/packages/runtime/client/rest/test/endpoints/transactions.spec.e2e.ts b/packages/runtime/client/rest/test/endpoints/transactions.spec.e2e.ts deleted file mode 100644 index 778466cb..00000000 --- a/packages/runtime/client/rest/test/endpoints/transactions.spec.e2e.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { mkFPTSRestClient } from "@marlowe.io/runtime-rest-client"; -import console from "console"; -import { getMarloweRuntimeUrl } from "../context.js"; - -global.console = console; - -describe("Transactions endpoints", () => { - const restClient = mkFPTSRestClient(getMarloweRuntimeUrl()); - - it( - "can navigate throught transaction headers" + - "(GET: /contracts/{contractd}/transactions)", - async () => { - // TODO - } - ); -}); diff --git a/packages/runtime/client/rest/test/endpoints/withdrawals.spec.e2e.ts b/packages/runtime/client/rest/test/endpoints/withdrawals.spec.e2e.ts deleted file mode 100644 index 83da3d34..00000000 --- a/packages/runtime/client/rest/test/endpoints/withdrawals.spec.e2e.ts +++ /dev/null @@ -1,12 +0,0 @@ -import console from "console"; - -global.console = console; - -describe("Withdrawals endpoints ", () => { - it( - "can navigate throught withdrawals headers" + "(GET: /withdrawals)", - async () => { - // TODO - } - ); -}); diff --git a/packages/runtime/client/rest/test/jest.e2e.config.mjs b/packages/runtime/client/rest/test/jest.e2e.config.mjs index e02d11b4..b76a7420 100644 --- a/packages/runtime/client/rest/test/jest.e2e.config.mjs +++ b/packages/runtime/client/rest/test/jest.e2e.config.mjs @@ -1,25 +1,50 @@ -import { fileURLToPath } from "node:url"; import dotenv from "dotenv" +import * as path from 'path'; +import * as fs from 'fs'; +function findRootDir(currentDir) { + // Check if a tsconfig.json file exists in the current directory + const tsconfigPath = path.join(currentDir, 'tsconfig-base.json'); + if (fs.existsSync(tsconfigPath)) { + return currentDir; + } + // If not, go up one level + const parentDir = path.dirname(currentDir); -const relative = (file) => fileURLToPath(new URL(file, import.meta.url)); + // Check if we have reached the root directory + if (parentDir === currentDir) { + return null; // Root not found + } + + // Recursive call to find root directory in the parent directory + return findRootDir(parentDir); +} + +// Get the root directory of the TypeScript project +const rootDir = findRootDir(process.cwd()); + +if (!rootDir) { + console.log(`Unable to find the root directory of the TypeScript project`); +} + +const packageDir = `${rootDir}/packages/runtime/client/rest` const moduleNameMapper = { '^(\\.{1,2}/.*)\\.js$': '$1', } -dotenv.config({ path: relative('../../../../../env/.env.test') }) +dotenv.config({ path: `${rootDir}/env/.env.test`}) const config = { testEnvironment: "node", displayName: "Runtime Rest Client e2e Tests", extensionsToTreatAsEsm: ['.ts'], testRegex: ".*e2e.ts$", - modulePaths: [relative('.')], + modulePaths: [packageDir], moduleNameMapper, transform: { - "^.+\\.ts$": ["ts-jest", { tsconfig:"test/tsconfig.json", useESM: true, isolatedModules: true }], + "^.+\\.ts$": ["ts-jest", { tsconfig:`${packageDir}/test/tsconfig.json`, useESM: true, isolatedModules: true }], }, }; diff --git a/packages/runtime/core/package.json b/packages/runtime/core/package.json index afc3771c..33a311e1 100644 --- a/packages/runtime/core/package.json +++ b/packages/runtime/core/package.json @@ -37,5 +37,8 @@ "fp-ts": "^2.16.1", "io-ts": "2.2.21", "newtype-ts": "0.3.5" + }, + "devDependencies": { + "@marlowe.io/testing-kit": "0.3.0-beta-rc1" } } diff --git a/packages/runtime/lifecycle/package.json b/packages/runtime/lifecycle/package.json index 4494cac2..560197f5 100644 --- a/packages/runtime/lifecycle/package.json +++ b/packages/runtime/lifecycle/package.json @@ -16,7 +16,7 @@ "build": "tsc --build src", "clean": "shx rm -rf dist", "test": "echo 'The legact runtime doesnt have unit tests'", - "test:e2e": "npm run clean && npm run build && NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --runInBand --config test/jest.e2e.config.mjs --verbose", + "test:e2e": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --runInBand --config test/jest.e2e.config.mjs --verbose", "build:e2e": "tsc --build test" }, "type": "module", @@ -58,5 +58,8 @@ "io-ts": "2.2.21", "newtype-ts": "0.3.5", "monocle-ts": "2.3.13" + }, + "devDependencies": { + "@marlowe.io/testing-kit": "0.3.0-beta-rc1" } } diff --git a/packages/runtime/lifecycle/src/nodejs/index.ts b/packages/runtime/lifecycle/src/nodejs/index.ts index f5614c71..4624eeff 100644 --- a/packages/runtime/lifecycle/src/nodejs/index.ts +++ b/packages/runtime/lifecycle/src/nodejs/index.ts @@ -2,24 +2,17 @@ import { mkFPTSRestClient, mkRestClient, } from "@marlowe.io/runtime-rest-client"; -import * as S from "@marlowe.io/wallet/nodejs"; +import * as Wallet from "@marlowe.io/wallet/lucid"; import * as Generic from "../generic/runtime.js"; +import { RuntimeLifecycle } from "../api.js"; +import { Lucid } from "lucid-cardano"; -export async function mkRuntimeLifecycle({ - runtimeURL, - context, - privateKeyBech32, -}: { - runtimeURL: string; - context: S.Context; - privateKeyBech32: string; -}) { - const wallet = await S.SingleAddressWallet.Initialise( - context, - privateKeyBech32 - ); - +export const mkRuntimeLifecycle = async ( + runtimeURL: string, + lucid: Lucid +): Promise => { + const wallet = await Wallet.mkLucidWallet(lucid); const deprecatedRestAPI = mkFPTSRestClient(runtimeURL); const restClient = mkRestClient(runtimeURL); return Generic.mkRuntimeLifecycle(deprecatedRestAPI, restClient, wallet); -} +}; diff --git a/packages/runtime/lifecycle/test/context.ts b/packages/runtime/lifecycle/test/context.ts deleted file mode 100644 index 86b3f8f0..00000000 --- a/packages/runtime/lifecycle/test/context.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Network, NetworkGuard, getNetwork } from "@marlowe.io/runtime-core"; -import { Context, getPrivateKeyFromHexString } from "@marlowe.io/wallet/nodejs"; -import { formatValidationErrors } from "jsonbigint-io-ts-reporters"; -import { unsafeEither, unsafeTaskEither } from "@marlowe.io/adapter/fp-ts"; -import * as E from "fp-ts/lib/Either.js"; - -export function getBlockfrostContext(): Context { - const { BLOCKFROST_URL, BLOCKFROST_PROJECT_ID } = process.env; - if (BLOCKFROST_URL == undefined) - throw "Test environment variable not defined (BLOCKFROST_URL)"; - if (BLOCKFROST_PROJECT_ID == undefined) - throw "Test environment variable not defined (BLOCKFROST_PROJECT_ID)"; - - return new Context( - BLOCKFROST_PROJECT_ID as string, - BLOCKFROST_URL as string, - getNetworkTestConfiguration() - ); -} - -export const getNetworkTestConfiguration = (): Network => { - const { NETWORK_NAME } = process.env; - if (NETWORK_NAME == undefined) - throw "Test environment variable not defined (NETWORK_NAME) "; - return unsafeEither( - E.mapLeft(formatValidationErrors)(NetworkGuard.decode(NETWORK_NAME)) - ); -}; - -export function getBankPrivateKey(): string { - const { BANK_PK_HEX } = process.env; - if (BANK_PK_HEX == undefined) - throw "environment configurations not available (BANK_PK_HEX)"; - return getPrivateKeyFromHexString(BANK_PK_HEX as string); -} - -export function getMarloweRuntimeUrl(): string { - const { MARLOWE_WEB_SERVER_URL } = process.env; - if (MARLOWE_WEB_SERVER_URL == undefined) - throw "environment configurations not available(MARLOWE_WEB_SERVER_URL)"; - return MARLOWE_WEB_SERVER_URL as string; -} diff --git a/packages/runtime/lifecycle/test/examples/swap.ada.token.e2e.spec.ts b/packages/runtime/lifecycle/test/examples/swap.ada.token.e2e.spec.ts index 596d9817..70c864da 100644 --- a/packages/runtime/lifecycle/test/examples/swap.ada.token.e2e.spec.ts +++ b/packages/runtime/lifecycle/test/examples/swap.ada.token.e2e.spec.ts @@ -1,33 +1,30 @@ import { pipe } from "fp-ts/lib/function.js"; import { addDays } from "date-fns"; -import { Deposit } from "@marlowe.io/language-core-v1/next"; - import { datetoTimeout, - adaValue, Input, MarloweState, } from "@marlowe.io/language-core-v1"; -import { - getBankPrivateKey, - getBlockfrostContext, - getMarloweRuntimeUrl, -} from "../context.js"; -import { provisionAnAdaAndTokenProvider } from "../provisionning.js"; + import console from "console"; import { ContractId, runtimeTokenToMarloweTokenValue, } from "@marlowe.io/runtime-core"; -import { onlyByContractIds } from "@marlowe.io/runtime-lifecycle/api"; import { MINUTES } from "@marlowe.io/adapter/time"; -import { - ContractDetails, - mintRole, -} from "@marlowe.io/runtime-rest-client/contract"; import { AtomicSwap } from "@marlowe.io/language-examples"; import { RestClient } from "@marlowe.io/runtime-rest-client"; +import { + generateSeedPhrase, + logDebug, + logInfo, + logWalletInfo, + readEnvConfigurationFile,mkTestEnvironment +} from "@marlowe.io/testing-kit"; +import { AxiosError } from "axios"; +import { MarloweJSON } from "@marlowe.io/adapter/codec"; + global.console = console; @@ -35,72 +32,92 @@ describe("swap", () => { it( "can execute the nominal case", async () => { - const provisionScheme = { - provider: { adaAmount: 20_000_000n }, - swapper: { - adaAmount: 20_000_000n, - tokenAmount: 50n, - tokenName: "TokenA", - }, - }; - - const { - tokenValueMinted, - adaProvider, - tokenProvider, - runtime, - restClient, - } = await provisionAnAdaAndTokenProvider( - getMarloweRuntimeUrl(), - getBlockfrostContext(), - getBankPrivateKey(), - provisionScheme - ); - const scheme: AtomicSwap.Scheme = { - offer: { - seller: { address: adaProvider.address }, - deadline: pipe(addDays(Date.now(), 1), datetoTimeout), - asset: adaValue(2n), - }, - ask: { - buyer: { role_token: "buyer" }, - deadline: pipe(addDays(Date.now(), 1), datetoTimeout), - asset: runtimeTokenToMarloweTokenValue(tokenValueMinted), - }, - swapConfirmation: { - deadline: pipe(addDays(Date.now(), 1), datetoTimeout), - }, - }; - - const swapContract = AtomicSwap.mkContract(scheme); - - const [contractId, txCreatedContract] = await runtime( - adaProvider - ).contracts.createContract({ - contract: swapContract, - roles: { - [scheme.ask.buyer.role_token]: mintRole(tokenProvider.address), - }, - }); - - await runtime(adaProvider).wallet.waitConfirmation(txCreatedContract); - - const inputHistory = await getInputHistory(restClient, contractId); - const marloweState = await getMarloweStatefromAnActiveContract( - restClient, - contractId - ); - const contractState = AtomicSwap.getActiveState( - scheme, - inputHistory, - marloweState - ); - const availableActions = AtomicSwap.getAvailableActions( - scheme, - contractState - ); - expect(contractState.typeName).toBe("WaitingSellerOffer"); - expect(availableActions.length).toBe(1); + try { + const { bank, runtime, participants } = + await readEnvConfigurationFile().then(mkTestEnvironment( + { + seller: { + walletSeedPhrase: generateSeedPhrase("24-words"), + scheme: { + lovelacesToTransfer: 25_000_000n, + assetsToMint: { tokenA: 15n }, + }, + }, + buyer: { + walletSeedPhrase: generateSeedPhrase("24-words"), + scheme: { + lovelacesToTransfer: 25_000_000n, + assetsToMint: { tokenB: 10n }, + }, + }, + } + )); + + const { seller, buyer } = participants; + + await logWalletInfo("seller", seller.wallet); + await logWalletInfo("buyer", buyer.wallet); + + const sellerAddress = await seller.wallet.getChangeAddress(); + const scheme: AtomicSwap.Scheme = { + offer: { + seller: { address: sellerAddress }, + deadline: pipe(addDays(Date.now(), 1), datetoTimeout), + asset: runtimeTokenToMarloweTokenValue( + seller.assetsProvisionned.tokens[0] + ), + }, + ask: { + buyer: { role_token: "buyer" }, + deadline: pipe(addDays(Date.now(), 1), datetoTimeout), + asset: runtimeTokenToMarloweTokenValue( + buyer.assetsProvisionned.tokens[0] + ), + }, + swapConfirmation: { + deadline: pipe(addDays(Date.now(), 1), datetoTimeout), + }, + }; + + const swapContract = AtomicSwap.mkContract(scheme); + logDebug(`contract: ${MarloweJSON.stringify(swapContract,null,4)}`); + + const [contractId, txCreatedContract] = await runtime + .mkLifecycle(seller.wallet) + .contracts.createContract({ + contract: swapContract, + minimumLovelaceUTxODeposit: 3_000_000, + roles: { + [scheme.ask.buyer.role_token]: sellerAddress, + }, + }); + logInfo("Contract Created"); + await seller.wallet.waitConfirmation(txCreatedContract); + await seller.wallet.waitRuntimeSyncingTillCurrentWalletTip( + runtime.client + ); + } catch (e) { + console.log(`catched : ${JSON.stringify(e)}`); + const error = e as AxiosError; + console.log(`catched : ${JSON.stringify(error.response?.data)}`); + expect(true).toBe(false); + } + // const inputHistory = await getInputHistory(restClient, contractId); + // const marloweState = await getMarloweStatefromAnActiveContract( + // restClient, + // contractId + // ); + // const contractState = AtomicSwap.getActiveState( + // scheme, + // inputHistory, + // marloweState + // ); + // const availableActions = AtomicSwap.getAvailableActions( + // scheme, + // contractState + // ); + // expect(contractState.typeName).toBe("WaitingSellerOffer"); + // expect(availableActions.length).toBe(1); // // Applying the first Deposit // let next = await runtime(adaProvider).contracts.getApplicableInputs( diff --git a/packages/runtime/lifecycle/test/generic/contracts.e2e.spec.ts b/packages/runtime/lifecycle/test/generic/contracts.e2e.spec.ts index ef435fe6..b49568bc 100644 --- a/packages/runtime/lifecycle/test/generic/contracts.e2e.spec.ts +++ b/packages/runtime/lifecycle/test/generic/contracts.e2e.spec.ts @@ -1,6 +1,6 @@ import { pipe } from "fp-ts/lib/function.js"; import { addDays } from "date-fns"; - +import { AxiosError } from "axios"; import { datetoTimeout, inputNotify, @@ -8,56 +8,75 @@ import { } from "@marlowe.io/language-core-v1"; import { oneNotifyTrue } from "@marlowe.io/language-examples"; -import { - getBankPrivateKey, - getBlockfrostContext, - getMarloweRuntimeUrl, -} from "../context.js"; -import { initialiseBankAndverifyProvisionning } from "../provisionning.js"; import console from "console"; import { MINUTES } from "@marlowe.io/adapter/time"; +import { + logError, + logInfo, + mkTestEnvironment, + readEnvConfigurationFile, +} from "@marlowe.io/testing-kit"; + global.console = console; -describe.skip("Runtime Contract Lifecycle ", () => { +describe("Runtime Contract Lifecycle ", () => { it( "can create a Marlowe Contract ", async () => { - const { runtime } = await initialiseBankAndverifyProvisionning( - getMarloweRuntimeUrl(), - getBlockfrostContext(), - getBankPrivateKey() - ); - const [contractId, txIdContractCreated] = - await runtime.contracts.createContract({ contract: close }); - await runtime.wallet.waitConfirmation(txIdContractCreated); - console.log("contractID created", contractId); + try { + const { bank, runtime } = await readEnvConfigurationFile().then(mkTestEnvironment({})) + const runtimeLifecycle = runtime.mkLifecycle(bank); + const [contractId, txIdContractCreated] = + await runtimeLifecycle.contracts.createContract({ + contract: close, + minimumLovelaceUTxODeposit: 3_000_000, + }); + await bank.waitConfirmation(txIdContractCreated); + logInfo(`contractID created : ${contractId}`); + } catch (e) { + const error = e as AxiosError; + logError(JSON.stringify(error.response?.data)); + logError(JSON.stringify(error)); + expect(true).toBe(false); + } }, 10 * MINUTES ), it( "can Apply Inputs to a Contract", async () => { - const { runtime } = await initialiseBankAndverifyProvisionning( - getMarloweRuntimeUrl(), - getBlockfrostContext(), - getBankPrivateKey() - ); - const notifyTimeout = pipe(addDays(Date.now(), 1), datetoTimeout); - const [contractId, txIdContractCreated] = - await runtime.contracts.createContract({ - contract: oneNotifyTrue(notifyTimeout), - }); - await runtime.wallet.waitConfirmation(txIdContractCreated); + try { + const { bank, runtime } = await readEnvConfigurationFile().then(mkTestEnvironment({})) + + const runtimeLifecycle = runtime.mkLifecycle(bank); - const txIdInputsApplied = await runtime.contracts.applyInputs( - contractId, - { - inputs: [inputNotify], - } - ); - const result = await runtime.wallet.waitConfirmation(txIdInputsApplied); - expect(result).toBe(true); + const notifyTimeout = pipe(addDays(Date.now(), 1), datetoTimeout); + const [contractId, txIdContractCreated] = + await runtimeLifecycle.contracts.createContract({ + contract: oneNotifyTrue(notifyTimeout), + minimumLovelaceUTxODeposit: 3_000_000, + }); + await bank.waitConfirmation(txIdContractCreated); + logInfo( + `contractID status : ${contractId} -> ${ + (await runtime.client.getContractById(contractId)).status + }` + ); + await bank.waitRuntimeSyncingTillCurrentWalletTip(runtime.client); + const txIdInputsApplied = + await runtimeLifecycle.contracts.applyInputs(contractId, { + inputs: [inputNotify], + }); + const result = await bank.waitConfirmation(txIdInputsApplied); + expect(result).toBe(true); + } catch (e) { + const error = e as AxiosError; + logError(error.message); + logError(JSON.stringify(error.response?.data)); + logError(JSON.stringify(error)); + expect(true).toBe(false); + } }, 10 * MINUTES ); diff --git a/packages/runtime/lifecycle/test/generic/payouts.e2e.spec.ts b/packages/runtime/lifecycle/test/generic/payouts.e2e.spec.ts index 2e8f0c0d..4e6fdd06 100644 --- a/packages/runtime/lifecycle/test/generic/payouts.e2e.spec.ts +++ b/packages/runtime/lifecycle/test/generic/payouts.e2e.spec.ts @@ -5,12 +5,6 @@ import { AtomicSwap } from "@marlowe.io/language-examples"; import { datetoTimeout, adaValue } from "@marlowe.io/language-core-v1"; import { Deposit } from "@marlowe.io/language-core-v1/next"; -import { - getBankPrivateKey, - getBlockfrostContext, - getMarloweRuntimeUrl, -} from "../context.js"; -import { provisionAnAdaAndTokenProvider } from "../provisionning.js"; import console from "console"; import { runtimeTokenToMarloweTokenValue } from "@marlowe.io/runtime-core"; import { onlyByContractIds } from "@marlowe.io/runtime-lifecycle/api"; @@ -20,95 +14,94 @@ import { mintRole } from "@marlowe.io/runtime-rest-client/contract"; global.console = console; describe.skip("Payouts", () => { - const provisionScheme = { - provider: { adaAmount: 20_000_000n }, - swapper: { adaAmount: 20_000_000n, tokenAmount: 50n, tokenName: "TokenA" }, - }; + // const provisionScheme = { + // provider: { adaAmount: 20_000_000n }, + // swapper: { adaAmount: 20_000_000n, tokenAmount: 50n, tokenName: "TokenA" }, + // }; - async function executeSwapWithRequiredWithdrawalTillClosing() { - const { tokenValueMinted, runtime, adaProvider, tokenProvider } = - await provisionAnAdaAndTokenProvider( - getMarloweRuntimeUrl(), - getBlockfrostContext(), - getBankPrivateKey(), - provisionScheme - ); - const scheme: AtomicSwap.Scheme = { - offer: { - seller: { address: adaProvider.address }, - deadline: pipe(addDays(Date.now(), 1), datetoTimeout), - asset: adaValue(2n), - }, - ask: { - buyer: { role_token: "buyer" }, - deadline: pipe(addDays(Date.now(), 1), datetoTimeout), - asset: runtimeTokenToMarloweTokenValue(tokenValueMinted), - }, - swapConfirmation: { - deadline: pipe(addDays(Date.now(), 1), datetoTimeout), - }, - }; + // async function executeSwapWithRequiredWithdrawalTillClosing() { + // const { tokenValueMinted, runtime, adaProvider, tokenProvider } = + // await provisionAnAdaAndTokenProvider( + // getMarloweRuntimeUrl(), + // getBlockfrostContext(), + // getBankPrivateKey(), + // provisionScheme + // ); + // const scheme: AtomicSwap.Scheme = { + // offer: { + // seller: { address: adaProvider.address }, + // deadline: pipe(addDays(Date.now(), 1), datetoTimeout), + // asset: adaValue(2n), + // }, + // ask: { + // buyer: { role_token: "buyer" }, + // deadline: pipe(addDays(Date.now(), 1), datetoTimeout), + // asset: runtimeTokenToMarloweTokenValue(tokenValueMinted), + // }, + // swapConfirmation: { + // deadline: pipe(addDays(Date.now(), 1), datetoTimeout), + // }, + // }; - const swapContract = AtomicSwap.mkContract(scheme); - const [contractId, txCreatedContract] = await runtime( - adaProvider - ).contracts.createContract({ - contract: swapContract, - roles: { [scheme.ask.buyer.role_token]: mintRole(tokenProvider.address) }, - }); + // const swapContract = AtomicSwap.mkContract(scheme); + // const [contractId, txCreatedContract] = await runtime( + // adaProvider + // ).contracts.createContract({ + // contract: swapContract, + // roles: { [scheme.ask.buyer.role_token]: mintRole(tokenProvider.address) }, + // }); - await runtime(adaProvider).wallet.waitConfirmation(txCreatedContract); + // await runtime(adaProvider).wallet.waitConfirmation(txCreatedContract); - // Applying the first Deposit - let next = await runtime(adaProvider).contracts.getApplicableInputs( - contractId - ); - const txFirstTokensDeposited = await runtime( - adaProvider - ).contracts.applyInputs(contractId, { - inputs: [pipe(next.applicable_inputs.deposits[0], Deposit.toInput)], - }); - await runtime(adaProvider).wallet.waitConfirmation(txFirstTokensDeposited); + // // Applying the first Deposit + // let next = await runtime(adaProvider).contracts.getApplicableInputs( + // contractId + // ); + // const txFirstTokensDeposited = await runtime( + // adaProvider + // ).contracts.applyInputs(contractId, { + // inputs: [pipe(next.applicable_inputs.deposits[0], Deposit.toInput)], + // }); + // await runtime(adaProvider).wallet.waitConfirmation(txFirstTokensDeposited); - // Applying the second Deposit - next = await runtime(tokenProvider).contracts.getApplicableInputs( - contractId - ); - await runtime(tokenProvider).contracts.applyInputs(contractId, { - inputs: [pipe(next.applicable_inputs.deposits[0], Deposit.toInput)], - }); - await runtime(tokenProvider).wallet.waitConfirmation( - txFirstTokensDeposited - ); + // // Applying the second Deposit + // next = await runtime(tokenProvider).contracts.getApplicableInputs( + // contractId + // ); + // await runtime(tokenProvider).contracts.applyInputs(contractId, { + // inputs: [pipe(next.applicable_inputs.deposits[0], Deposit.toInput)], + // }); + // await runtime(tokenProvider).wallet.waitConfirmation( + // txFirstTokensDeposited + // ); - return { - contractId, - runtime, - adaProvider, - tokenProvider, - }; - } + // return { + // contractId, + // runtime, + // adaProvider, + // tokenProvider, + // }; + // } it( "Payouts can be withdrawn", async () => { - const result = await executeSwapWithRequiredWithdrawalTillClosing(); - const { adaProvider, tokenProvider, contractId, runtime } = result; - const adaProviderPayouts = await runtime(adaProvider).payouts.available( - onlyByContractIds([contractId]) - ); - expect(adaProviderPayouts.length).toBe(1); - await runtime(adaProvider).payouts.withdraw([ - adaProviderPayouts[0].payoutId, - ]); - - const tokenProviderPayouts = await runtime( - tokenProvider - ).payouts.available(onlyByContractIds([contractId])); - expect(tokenProviderPayouts.length).toBe(1); - await runtime(tokenProvider).payouts.withdraw([ - tokenProviderPayouts[0].payoutId, - ]); + // const result = await executeSwapWithRequiredWithdrawalTillClosing(); + // const { adaProvider, tokenProvider, contractId, runtime } = result; + // const adaProviderPayouts = await runtime(adaProvider).payouts.available( + // onlyByContractIds([contractId]) + // ); + // expect(adaProviderPayouts.length).toBe(1); + // await runtime(adaProvider).payouts.withdraw([ + // adaProviderPayouts[0].payoutId, + // ]); + // const tokenProviderPayouts = await runtime( + // tokenProvider + // ).payouts.available(onlyByContractIds([contractId])); + // expect(tokenProviderPayouts.length).toBe(1); + // await runtime(tokenProvider).payouts.withdraw([ + // tokenProviderPayouts[0].payoutId, + // ]); }, 10 * MINUTES ); diff --git a/packages/runtime/lifecycle/test/jest.e2e.config.mjs b/packages/runtime/lifecycle/test/jest.e2e.config.mjs index 7e493c72..97d7cd52 100644 --- a/packages/runtime/lifecycle/test/jest.e2e.config.mjs +++ b/packages/runtime/lifecycle/test/jest.e2e.config.mjs @@ -1,23 +1,51 @@ -import { fileURLToPath } from "node:url"; import dotenv from "dotenv" -const relative = (file) => fileURLToPath(new URL(file, import.meta.url)); +import * as path from 'path'; +import * as fs from 'fs'; + +function findRootDir(currentDir) { + // Check if a tsconfig.json file exists in the current directory + const tsconfigPath = path.join(currentDir, 'tsconfig-base.json'); + if (fs.existsSync(tsconfigPath)) { + return currentDir; + } + + // If not, go up one level + const parentDir = path.dirname(currentDir); + + // Check if we have reached the root directory + if (parentDir === currentDir) { + return null; // Root not found + } + + // Recursive call to find root directory in the parent directory + return findRootDir(parentDir); +} + +// Get the root directory of the TypeScript project +const rootDir = findRootDir(process.cwd()); + +if (!rootDir) { + console.log(`Unable to find the root directory of the TypeScript project`); +} + +const packageDir = `${rootDir}/packages/runtime/lifecycle` const moduleNameMapper = { '^(\\.{1,2}/.*)\\.js$': '$1', } -dotenv.config({ path: relative('../../../../env/.env.test') }) +dotenv.config({ path: `${rootDir}/env/.env.test`}) const config = { testEnvironment: "node", displayName: "Runtime Lifecycle e2e Test" , extensionsToTreatAsEsm: ['.ts'], testRegex: ".*e2e.spec.ts$", - // modulePaths: [relative('.')], + modulePaths: [packageDir], moduleNameMapper, transform: { - "^.+\\.ts$": ["ts-jest", { tsconfig:relative("tsconfig.json"), useESM: true, isolatedModules: true }], + "^.+\\.ts$": ["ts-jest", { tsconfig:`${packageDir}/test/tsconfig.json`, useESM: true, isolatedModules: true }], }, }; diff --git a/packages/runtime/lifecycle/test/provisionning.ts b/packages/runtime/lifecycle/test/provisionning.ts deleted file mode 100644 index 0bc1863a..00000000 --- a/packages/runtime/lifecycle/test/provisionning.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { TokenName } from "@marlowe.io/language-core-v1"; -import { mkRuntimeLifecycle } from "@marlowe.io/runtime-lifecycle/generic"; -import { - mkFPTSRestClient, - mkRestClient, -} from "@marlowe.io/runtime-rest-client"; -import { - Context, - SingleAddressWallet, - PrivateKeysAsHex, -} from "@marlowe.io/wallet/nodejs"; -import { assetIdToString } from "@marlowe.io/runtime-core"; -import { WalletAPI } from "@marlowe.io/wallet/api"; - -const log = (message: string) => console.log(`\t## - ${message}`); -const formatADA = (lovelaces: bigint): String => - new Intl.NumberFormat().format(lovelaces / 1_000_000n).concat(" ₳"); - -export type ProvisionScheme = { - provider: { adaAmount: bigint }; - swapper: { adaAmount: bigint; tokenAmount: bigint; tokenName: TokenName }; -}; - -export async function provisionAnAdaAndTokenProvider( - runtimeURL: string, - walletContext: Context, - bankPrivateKey: PrivateKeysAsHex, - scheme: ProvisionScheme -) { - const deprecatedRestAPI = mkFPTSRestClient(runtimeURL); - const restClient = mkRestClient(runtimeURL); - - // Generating/Initialising Accounts - const bank = await SingleAddressWallet.Initialise( - walletContext, - bankPrivateKey - ); - const adaProvider = await SingleAddressWallet.Random(walletContext); - const tokenProvider = await SingleAddressWallet.Random(walletContext); - log(`Check Bank treasury`); - const bankBalance = await bank.getLovelaces(); - log(`Bank (${bank.address})`); - log(` - ${formatADA(bankBalance)}`); - - expect(bankBalance).toBeGreaterThan(100_000_000); - - log(`Provisionning testing accounts`); - log(`Seller ${adaProvider.address}`); - log(`Buyer ${tokenProvider.address}`); - - await bank.provision([ - [adaProvider, scheme.provider.adaAmount], - [tokenProvider, scheme.swapper.adaAmount], - ]); - - log(`Ada provisionning Done`); - const adaProviderBalance = await adaProvider.getLovelaces(); - const tokenProviderADABalance = await tokenProvider.getLovelaces(); - - log(`Seller (${adaProvider.address}`); - log(` - ${formatADA(adaProviderBalance)}`); - log(`Buyer (${tokenProvider.address})`); - log(` - ${formatADA(tokenProviderADABalance)}`); - - log(`Minting new Random Tokens`); - const tokenValueMinted = await tokenProvider.mintRandomTokens( - scheme.swapper.tokenName, - scheme.swapper.tokenAmount - ); - - const tokenBalance = await tokenProvider.tokenBalance( - tokenValueMinted.assetId - ); - - log(`Token Provider (${tokenProvider.address})`); - log(` - ${formatADA(tokenProviderADABalance)}`); - log(` - ${tokenBalance} ${assetIdToString(tokenValueMinted.assetId)}`); - - expect(tokenBalance).toBe(scheme.swapper.tokenAmount); - - return { - adaProvider: adaProvider, - tokenProvider: tokenProvider, - tokenValueMinted: tokenValueMinted, - restClient: restClient, - runtime: (wallet: WalletAPI) => - mkRuntimeLifecycle(deprecatedRestAPI, restClient, wallet), - }; -} - -export async function initialiseBankAndverifyProvisionning( - runtimeURL: string, - walletContext: Context, - bankPrivateKey: PrivateKeysAsHex -) { - const deprecatedRestAPI = mkFPTSRestClient(runtimeURL); - const restClient = mkRestClient(runtimeURL); - - const bank = await SingleAddressWallet.Initialise( - walletContext, - bankPrivateKey - ); - const bankBalance = await bank.getLovelaces(); - - // Check Banks treasury - log(`Bank (${bank.address})`); - log(` - ${formatADA(bankBalance)}`); - - expect(bankBalance).toBeGreaterThan(100_000_000); - - return { - bank: bank, - restClient: restClient, - runtime: mkRuntimeLifecycle(deprecatedRestAPI, restClient, bank), - }; -} diff --git a/packages/testing-kit/.npmignore b/packages/testing-kit/.npmignore new file mode 100644 index 00000000..dbc86908 --- /dev/null +++ b/packages/testing-kit/.npmignore @@ -0,0 +1 @@ +/src \ No newline at end of file diff --git a/packages/testing-kit/Readme.md b/packages/testing-kit/Readme.md new file mode 100644 index 00000000..cfda9b33 --- /dev/null +++ b/packages/testing-kit/Readme.md @@ -0,0 +1,3 @@ +# Description + +A Set of functionalities that supports @marlowe.io libraries for testing purposes. diff --git a/packages/testing-kit/package.json b/packages/testing-kit/package.json new file mode 100644 index 00000000..932be03c --- /dev/null +++ b/packages/testing-kit/package.json @@ -0,0 +1,44 @@ +{ + "name": "@marlowe.io/testing-kit", + "version": "0.3.0-beta-rc1", + "description": "Testing libraries to Support Marlowe Development", + "repository": "https://github.com/input-output-hk/marlowe-ts-sdk", + "publishConfig": { + "access": "public" + }, + "contributors": [ + "Nicolas Henin (https://iohk.io)", + "Hernan Rajchert (https://iohk.io)", + "Bjorn Kihlberg (https://iohk.io)" + ], + "license": "Apache-2.0", + "scripts": { + "build": "tsc --build src", + "clean": "shx rm -rf dist", + "genSeedPhrase" : "npm run build && node ./dist/esm/executable/generateSeedPhrase.js", + "test": "echo 'testing-kit doesnt have tests for the moment'" + }, + "type": "module", + "module": "./dist/esm/index.js", + "main": "./dist/esm/index.js", + "types": "./dist/esm/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "import": "./dist/esm/index.js", + "require": "./dist/bundled/cjs/index.cjs", + "types": "./dist/esm/index.d.ts" + } + }, + "dependencies": { + "@marlowe.io/wallet": "0.3.0-beta-rc1", + "@marlowe.io/runtime-core": "0.3.0-beta-rc1", + "@marlowe.io/runtime-rest-client": "0.3.0-beta-rc1", + "@marlowe.io/adapter": "0.3.0-beta-rc1", + "@marlowe.io/language-core-v1": "0.3.0-beta-rc1", + "bip39": "3.1.0", + "fp-ts": "2.16.1" + } +} diff --git a/packages/testing-kit/src/environment/configuration.ts b/packages/testing-kit/src/environment/configuration.ts new file mode 100644 index 00000000..c39f52fc --- /dev/null +++ b/packages/testing-kit/src/environment/configuration.ts @@ -0,0 +1,116 @@ +import { Network, NetworkGuard, getNetwork } from "@marlowe.io/runtime-core"; +import { formatValidationErrors } from "jsonbigint-io-ts-reporters"; +import { unsafeEither } from "@marlowe.io/adapter/fp-ts"; +import * as E from "fp-ts/lib/Either.js"; +import { Blockfrost } from "lucid-cardano"; +import { SeedPhrase, seedPhrase } from "../wallet/seedPhrase.js"; +import { + RestClient, + mkRestClient, + RuntimeVersion, +} from "@marlowe.io/runtime-rest-client"; +import * as Lucid from "lucid-cardano"; +import * as G from "@marlowe.io/runtime-rest-client/guards"; +import { MarloweJSON } from "@marlowe.io/adapter/codec"; + +/** + * Test Configuration : Read from an env file, it contains : + * 1. The necessary configuration to run e2e tests over a Lucid Wallet and a Runtime. + * 2. A Bank Wallet Seed Phrase to provision ephemeral Test Wallets + */ +export type TestConfiguration = { + bank: { seedPhrase: SeedPhrase }; + lucid: { blockfrost: Blockfrost; node: { network: Lucid.Network } }; + runtime: { + version: RuntimeVersion; + url: URL; + client: RestClient; + node: { network: Network }; + }; +}; + +/** + * Read Test Configurations from an env file + * @returns + */ +export const readEnvConfigurationFile = async (): Promise => { + const runtimeURL = readEnvRuntimeURL(); + const lucidNetwork = readEnvLucidNodeNetwork(); + const runtimeClient = mkRestClient(runtimeURL.toString()); + const status = await runtimeClient.getRuntimeStatus(); + const runtimeNodeNetwork = getNetwork(status.networkId); + + if (!G.CompatibleRuntimeVersion.is(status.version)) { + throw { + message: "Runtime Version is not Compatible with the ts-sdk", + details: MarloweJSON.stringify(status), + }; + } + + const configuration = { + bank: { seedPhrase: readEnvBankSeedPhrase() }, + lucid: { + blockfrost: readEnvBlockfrost(), + node: { network: toLucidNetwork(lucidNetwork) }, + }, + runtime: { + version: status.version, + url: runtimeURL, + client: mkRestClient(runtimeURL.toString()), + node: { network: runtimeNodeNetwork }, + }, + }; + return configuration; +}; + +const readEnvBlockfrost = (): Blockfrost => { + const { BLOCKFROST_URL, BLOCKFROST_PROJECT_ID } = process.env; + if (BLOCKFROST_URL == undefined) + throw "Test environment variable not defined (BLOCKFROST_URL)"; + if (BLOCKFROST_PROJECT_ID == undefined) + throw "Test environment variable not defined (BLOCKFROST_PROJECT_ID)"; + return new Blockfrost(BLOCKFROST_URL, BLOCKFROST_PROJECT_ID); +}; + +const readEnvLucidNodeNetwork = (): Network => { + const { NETWORK_NAME } = process.env; + if (NETWORK_NAME == undefined) + throw "Test environment variable not defined (NETWORK_NAME) "; + return unsafeEither( + E.mapLeft(formatValidationErrors)(NetworkGuard.decode(NETWORK_NAME)) + ); +}; + +const readEnvBankSeedPhrase = (): SeedPhrase => { + const { BANK_SEED_PHRASE } = process.env; + if (BANK_SEED_PHRASE !== undefined) { + return seedPhrase(JSON.parse(BANK_SEED_PHRASE)); + } else { + throw "environment configurations not available (BANK_PK_HEX)"; + } +}; + +const readEnvRuntimeURL = () => { + const { MARLOWE_WEB_SERVER_URL } = process.env; + if (MARLOWE_WEB_SERVER_URL == undefined) + throw "environment configurations not available(MARLOWE_WEB_SERVER_URL)"; + return new URL(MARLOWE_WEB_SERVER_URL); +}; + +/** + * Convert a Marlowe Network Model to a Lucid one. + * @param network + * @returns + */ +const toLucidNetwork = (network: Network): Lucid.Network => { + switch (network) { + case "private": + return "Custom"; + case "preview": + return "Preview"; + case "preprod": + return "Preprod"; + case "mainnet": + return "Mainnet"; + } +}; \ No newline at end of file diff --git a/packages/testing-kit/src/environment/index.ts b/packages/testing-kit/src/environment/index.ts new file mode 100644 index 00000000..807455d3 --- /dev/null +++ b/packages/testing-kit/src/environment/index.ts @@ -0,0 +1,100 @@ +import { mkRuntimeLifecycle } from "@marlowe.io/runtime-lifecycle/generic"; +import { RestClient, mkFPTSRestClient } from "@marlowe.io/runtime-rest-client"; + +import { Lucid } from "lucid-cardano"; + +import { RuntimeLifecycle } from "@marlowe.io/runtime-lifecycle/api"; +import { Assets } from "@marlowe.io/runtime-core"; +import { TestConfiguration, logInfo, logWalletInfo, mkLucidWalletTest } from "@marlowe.io/testing-kit"; +import { ProvisionRequest, WalletTestAPI } from "../wallet/api.js"; + + +/** + * List of Participants available for a test + */ +export type TestEnvironment = { + /** + * Bank Wallet + */ + bank: WalletTestAPI; + /** + * List of Participants available for the test + */ + participants: Participants; + /** + * Access to runtime client and the runtime lifecycle api + */ + runtime: { + client: RestClient; + mkLifecycle: (wallet: WalletTestAPI) => RuntimeLifecycle; + }; +}; + +/** + * List of Participants available for a test + */ +export type Participants = { + [participant: string]: + + { /** + * Wallet Test instance + */ + wallet: WalletTestAPI, + /** + * List of Assets provisionned By the Bank Wallet + */ + assetsProvisionned: Assets }; +}; + +/** + * Provide a Test Environment to execute E2E tests over a Lucid Wallet and an instance of a + * Marlowe Runtime. + * @param provisionRequest + * @returns + */ +export const mkTestEnvironment = + (provisionRequest: ProvisionRequest) => + async (testConfiguration: TestConfiguration): Promise => { + logInfo("Test Environment : Initiating"); + + const deprecatedRestAPI = mkFPTSRestClient( + testConfiguration.runtime.url.toString() + ); + + const bankLucid = await Lucid.new( + testConfiguration.lucid.blockfrost, + testConfiguration.lucid.node.network + ); + + bankLucid.selectWalletFromSeed(testConfiguration.bank.seedPhrase.join(" ")); + + const bank = mkLucidWalletTest(bankLucid); + + await logWalletInfo("bank", bank); + + const bankBalance = await bank.getLovelaces(); + if(bankBalance <= 100_000_000n) { + throw { message: "Bank is not sufficiently provisionned (< 100 Ada)"} + } + logInfo("Bank is provisionned enough"); + const participants = await bank.provision(provisionRequest); + + await bank.waitRuntimeSyncingTillCurrentWalletTip( + testConfiguration.runtime.client + ); + + logInfo("Test Environment : Ready"); + return { + bank, + participants: participants, + runtime: { + client: testConfiguration.runtime.client, + mkLifecycle: (wallet: WalletTestAPI) => + mkRuntimeLifecycle( + deprecatedRestAPI, + testConfiguration.runtime.client, + wallet + ), + }, + }; + }; diff --git a/packages/testing-kit/src/executable/generateSeedPhrase.ts b/packages/testing-kit/src/executable/generateSeedPhrase.ts new file mode 100644 index 00000000..98105bee --- /dev/null +++ b/packages/testing-kit/src/executable/generateSeedPhrase.ts @@ -0,0 +1,18 @@ +import { mkLucidWallet } from "@marlowe.io/wallet"; +import { generateSeedPhrase } from "../index.js"; +import { MarloweJSON } from "@marlowe.io/adapter/codec"; + + +const log = console.log.bind(console); +/** + * Little Executable to generate randomly a new 24-words seed phrase for creating a new wallet. + */ +async function main() { + generateSeedPhrase("24-words"); + log(` * Generating a new 24-words seed phrase :`) + log(MarloweJSON.stringify(generateSeedPhrase("24-words"),null,4)) + log("Done.🎉"); +} + +await main(); + diff --git a/packages/testing-kit/src/index.ts b/packages/testing-kit/src/index.ts new file mode 100644 index 00000000..32e03227 --- /dev/null +++ b/packages/testing-kit/src/index.ts @@ -0,0 +1,5 @@ +export * from "./wallet/lucid/index.js"; +export * from "./wallet/seedPhrase.js"; +export * from "./logging.js"; +export * from "./environment/configuration.js"; +export * from "./environment/index.js" diff --git a/packages/testing-kit/src/logging.ts b/packages/testing-kit/src/logging.ts new file mode 100644 index 00000000..fcebfec3 --- /dev/null +++ b/packages/testing-kit/src/logging.ts @@ -0,0 +1,34 @@ +import { MarloweJSON } from "@marlowe.io/adapter/codec"; +import { WalletTestAPI } from "./wallet/api.js"; + +export const logDebug = (message: string) => + (process.env.LOG_DEBUG_LEVEL !== undefined + && JSON.parse(process.env.LOG_DEBUG_LEVEL) === true )?console.log(`## ||| [${message}]`):{}; + +export const logInfo = (message: string) => console.log(`## ${message}`); + +export const logWarning = (message: string) => + console.log(`## << ${message} >>`); + +export const logError = (message: string) => + console.log(`## !! [${message}] !!`); + + + /** + * Logging utility for a Wallet Test API instance + * @param walletName + * @param wallet + */ +export const logWalletInfo = async ( + walletName: string, + wallet: WalletTestAPI +) => { + const address = await wallet.getChangeAddress(); + const lovelaces = await wallet.getLovelaces(); + const tokens = await wallet.getTokens(); + logInfo(`Wallet ${walletName}`); + logInfo(` - Address : ${address}`); + logInfo(` - Lovelaces : ${lovelaces}`); + logInfo(` - Tokens : ${MarloweJSON.stringify(tokens)}`); +}; + \ No newline at end of file diff --git a/packages/testing-kit/src/tsconfig.json b/packages/testing-kit/src/tsconfig.json new file mode 100644 index 00000000..8a18dc82 --- /dev/null +++ b/packages/testing-kit/src/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../tsconfig-base.json", + "compilerOptions": { + "outDir": "../dist/esm", + "paths": { + "@marlowe.io/adapter/*": ["../../adapter/src/*"], + "@marlowe.io/runtime-core/*": ["../../runtime/core/src/*"], + "@marlowe.io/wallet/*": ["../../wallet/src/*"], + "@marlowe.io/language-core-v1/*": ["../../language/core/v1/src/*"] + } + }, + "references": [ + { "path": "../../adapter/src" }, + { "path": "../../wallet/src" }, + { "path": "../../language/core/v1/src" }, + { "path": "../../runtime/core/src" } + ] +} diff --git a/packages/testing-kit/src/wallet/api.ts b/packages/testing-kit/src/wallet/api.ts new file mode 100644 index 00000000..f8905dca --- /dev/null +++ b/packages/testing-kit/src/wallet/api.ts @@ -0,0 +1,80 @@ +/** + * This module provides {@link @marlowe.io/wallet!api.WalletAPI} extended capabilities for + * testing purposes. It is used for E2E testing in 2 paricular `@marlowe.io` packages : + * - {@link @marlowe.io/runtime-rest-client} + * - {@link @marlowe.io/runtime-lifecycle} + * @packageDocumentation + */ +import { WalletAPI } from "@marlowe.io/wallet"; +import { RestClient } from "@marlowe.io/runtime-rest-client"; +import { SeedPhrase } from "./seedPhrase.js"; +import * as RuntimeCore from "@marlowe.io/runtime-core"; + +/** + * The WalletTestAPI is an extended {@link @marlowe.io/wallet!api.WalletAPI} for interacting with a Cardano wallet in a + * Test Environment. + */ +export interface WalletTestAPI extends WalletAPI { + /** + * Execute a Provisioning Scheme from this current WalletTestAPI instance for the simulated participants of a Test: + * - Minting Tokens with a basic policy id to Participants + * - Transfering Lovelaces to Participants + * @param request + */ + provision(request: ProvisionRequest): Promise; + /** + * Wait if the runtime is behind the current slot of the wallet Test. + * @remarks + * Wallets and Marlowe Runtimes are potentially connected to 2 differents Cardano Nodes. + * In our test environment, in order to avoid inconsistencies when building Tx, we need + * to provide some synchronization mechanism. Depending on which component is performing the last + * Tx, we need to wait for the other one to catch up to have a consustent behaviour. + * @param client + */ + waitRuntimeSyncingTillCurrentWalletTip(client: RestClient): Promise; + // TODO : waitWalletSyncingTillCurrentRuntimeTip +} + +/** + * Provision Request on a given WalletTestAPI instance + */ +export type ProvisionRequest = { + [participant: string]: { + /** + * SeedPhrase of the participant wallet. + */ + walletSeedPhrase: SeedPhrase; + /** + * Provisionning Scheme + */ + scheme: ProvisionScheme; + }; +}; + +/** + * Provision Response on a given WalletTestAPI instance (see Request) + */ +export type ProvisionResponse = { + [participant: string]: { + wallet: WalletTestAPI; + assetsProvisionned: RuntimeCore.Assets; + }; +}; + +/** + * Provisionnibg Scheme on a given WalletTestAPI instance + */ +export type ProvisionScheme = { + lovelacesToTransfer: RuntimeCore.AssetQuantity; + assetsToMint: MintingScheme; +}; + +/** + * Minting Scheme on a given WalletTestAPI instance + */ +export type MintingScheme = { + [assetName: RuntimeCore.AssetName]: RuntimeCore.AssetQuantity; +}; + + + diff --git a/packages/testing-kit/src/wallet/lucid/index.ts b/packages/testing-kit/src/wallet/lucid/index.ts new file mode 100644 index 00000000..3e3b62e0 --- /dev/null +++ b/packages/testing-kit/src/wallet/lucid/index.ts @@ -0,0 +1,80 @@ +/** + * This module provides {@link @marlowe.io/wallet!api.WalletAPI} extended capabilities for + * testing purposes. It is used for E2E testing in 2 paricular `@marlowe.io` packages : + * - {@link @marlowe.io/runtime-rest-client} + * - {@link @marlowe.io/runtime-lifecycle} + * @packageDocumentation + */ + +import { Lucid } from "lucid-cardano"; + +import { WalletAPI, mkLucidWallet } from "@marlowe.io/wallet"; + +import { RestClient } from "@marlowe.io/runtime-rest-client"; +import { logWarning } from "../../logging.js"; + +export * as Provision from "./provisionning.js"; +import * as Provision from "./provisionning.js"; +import { sleep, waitForPredicatePromise } from "@marlowe.io/adapter/time"; +import { WalletTestAPI } from "../api.js"; + +/** + * @description Dependency Injection for the WalletTestAPI implemented with Lucid + * @hidden + */ +export type WalletTestDI = { lucid: Lucid; wallet: WalletAPI }; + +export function mkLucidWalletTest(lucidWallet: Lucid): WalletTestAPI { + const di = { lucid: lucidWallet, wallet: mkLucidWallet(lucidWallet) }; + return { + ...di.wallet, + ...{ provision: Provision.provision(di) }, + ...{ + waitRuntimeSyncingTillCurrentWalletTip: + waitRuntimeSyncingTillCurrentWalletTip(di), + }, + }; +} + +/** + * `waitRuntimeSyncingTillCurrentWalletTip` implementation using a Lucid Wallet + * @remarks + * This implementation is approximative because we are waiting for the runtime chain to sync and + * not the runtime itself. The Runtime doesn't provide a tip representing the last slot read but + * it provides the last slot where a contract Tx activity has been read. + * We are adding a sleep at the end, to artificially wait the runtime to sync on a synced Runtime Chain. + * @param client + * @param aSlotNo + * @returns + */ +const waitRuntimeSyncingTillCurrentWalletTip = + (di: WalletTestDI) => + async (client: RestClient): Promise => { + const { lucid } = di; + const currentLucidSlot = BigInt(lucid.currentSlot()); + await waitForPredicatePromise( + isRuntimeChainMoreAdvancedThan(client, currentLucidSlot) + ); + return sleep(5); + }; + +/** + * Predicate that verify is the Runtime Chain Tip >= to a givent slot + * @param client + * @param aSlotNo + * @returns + */ +export const isRuntimeChainMoreAdvancedThan = + (client: RestClient, aSlotNo: bigint) => () => + client.getRuntimeStatus().then((status) => { + if (status.tips.runtimeChain.blockHeader.slotNo >= aSlotNo) { + return true; + } else { + logWarning( + `Waiting Runtime to be Synced (Delta ${ + status.tips.runtimeChain.blockHeader.slotNo - aSlotNo + }) ` + ); + return false; + } + }); diff --git a/packages/testing-kit/src/wallet/lucid/provisionning.ts b/packages/testing-kit/src/wallet/lucid/provisionning.ts new file mode 100644 index 00000000..d4e97cfb --- /dev/null +++ b/packages/testing-kit/src/wallet/lucid/provisionning.ts @@ -0,0 +1,181 @@ +import { + Lucid, + toUnit, + fromText, + NativeScript, + Script, + Assets as LucidAssets, +} from "lucid-cardano"; +import { addDays } from "date-fns"; + +import { mergeAssets } from "@marlowe.io/adapter/lucid"; + +import * as RuntimeCore from "@marlowe.io/runtime-core"; +import { pipe } from "fp-ts/lib/function.js"; +import * as A from "fp-ts/lib/Array.js"; +import { MarloweJSON } from "@marlowe.io/adapter/codec"; +import { logDebug, logInfo } from "../../logging.js"; + +import { WalletTestDI, mkLucidWalletTest } from "./index.js"; +import { ProvisionRequest, ProvisionResponse, ProvisionScheme, WalletTestAPI } from "../api.js"; + +type RequestWithWallets = { + [participant: string]: { + wallet: WalletTestAPI; + scheme: ProvisionScheme; + }; +}; + +export const provision = + (di: WalletTestDI) => + async (request: ProvisionRequest): Promise => { + if (Object.entries(request).length === 0) { + logInfo("No Participants Involved") + return Promise.resolve({}) + } + const { lucid, wallet } = di; + let requestWithWallets: RequestWithWallets = {}; + await Promise.all( + Object.entries(request).map(([participant, {walletSeedPhrase, scheme}]) => { + return Lucid + .new(lucid.provider,lucid.network) + .then((newLucidInstance) => { + const wallet = mkLucidWalletTest(newLucidInstance.selectWalletFromSeed(walletSeedPhrase.join(` `))); + requestWithWallets[participant] = { wallet, scheme }; + }); + }) + ); + + logInfo( + `Provisionning Request : ${MarloweJSON.stringify( + requestWithWallets, + null, + 4 + )}` + ); + + const mintingDeadline = addDays(Date.now(), 1); + + const [script, policyId] = await mkPolicyWithDeadlineAndOneAuthorizedSigner( + di + )(mintingDeadline); + + const distributions = await Promise.all( + Object.entries(requestWithWallets).map(([participant, x]) => + x.wallet.getChangeAddress().then( + (address) => + [ + participant, + x.wallet, + RuntimeCore.addressBech32(address), + { + lovelaces: x.scheme.lovelacesToTransfer, + tokens: Object.entries(x.scheme.assetsToMint).map( + ([assetName, quantity]) => ({ + quantity, + assetId: { assetName, policyId }, + }) + ), + }, + ] as [ + string, + WalletTestAPI, + RuntimeCore.AddressBech32, + RuntimeCore.Assets + ] + ) + ) + ); + + const assetsToMint = pipe( + distributions, + A.map((aDistribution) => toAssetsToMint(aDistribution[3])), + A.reduce(mergeAssets.empty, mergeAssets.concat) + ); + + logDebug(`Distribution : ${MarloweJSON.stringify(distributions,null,4)}`); + logDebug(`Assets to mint : ${MarloweJSON.stringify(assetsToMint,null,4)}`); + + const mintTx = lucid + .newTx() + .mintAssets(assetsToMint) + .validTo(Date.now() + 100000) + .attachMintingPolicy(script); + + const transferTx = await pipe( + distributions, + A.reduce( + lucid.newTx(), + (tx, aDistribution) => + tx + .payToAddress( + aDistribution[2], + toAssetsToTransfer(aDistribution[3]) + ) + .payToAddress(aDistribution[2], { lovelace: 5_000_000n }) + .payToAddress(aDistribution[2], { lovelace: 5_000_000n }) + .payToAddress(aDistribution[2], { lovelace: 5_000_000n }) // add a Collateral + ) + ); + + var result: ProvisionResponse = {}; + distributions.map(([participant, wallet, , assetsProvisionned]) => { + result[participant] = { + wallet: wallet, + assetsProvisionned: assetsProvisionned, + }; + }); + logDebug(`result : ${MarloweJSON.stringify(result,null,4)}`); + const provisionTx = await mintTx.compose(transferTx).complete(); + + await provisionTx + .sign() + .complete() + .then((tx) => tx.submit()) + .then((txHashSubmitted) => wallet.waitConfirmation(txHashSubmitted)); + return result; + }; + +const mkPolicyWithDeadlineAndOneAuthorizedSigner = + ({ lucid }: WalletTestDI) => + async (deadline: Date): Promise<[Script, RuntimeCore.PolicyId]> => { + const { paymentCredential } = lucid.utils.getAddressDetails( + await lucid.wallet.address() + ); + const json: NativeScript = { + type: "all", + scripts: [ + { + type: "before", + slot: lucid.utils.unixTimeToSlot(deadline.valueOf()), + }, + { type: "sig", keyHash: paymentCredential?.hash! }, + ], + }; + const script = lucid.utils.nativeScriptFromJson(json); + const policyId = lucid.utils.mintingPolicyToId(script); + return [script, RuntimeCore.policyId(policyId)]; + }; + + const toAssetsToTransfer = (assets: RuntimeCore.Assets): LucidAssets => { + var lucidAssets: { [key: string]: bigint } = {}; + lucidAssets["lovelace"] = assets.lovelaces; + assets.tokens.map( + (token) => + (lucidAssets[ + toUnit(token.assetId.policyId, fromText(token.assetId.assetName)) + ] = token.quantity) + ); + return lucidAssets; + }; + + const toAssetsToMint = (assets: RuntimeCore.Assets): LucidAssets => { + var lucidAssets: { [key: string]: bigint } = {}; + assets.tokens.map( + (token) => + (lucidAssets[ + toUnit(token.assetId.policyId, fromText(token.assetId.assetName)) + ] = token.quantity) + ); + return lucidAssets; + }; \ No newline at end of file diff --git a/packages/testing-kit/src/wallet/seedPhrase.ts b/packages/testing-kit/src/wallet/seedPhrase.ts new file mode 100644 index 00000000..cb96122f --- /dev/null +++ b/packages/testing-kit/src/wallet/seedPhrase.ts @@ -0,0 +1,29 @@ +import { unsafeEither } from "@marlowe.io/adapter/fp-ts"; +import * as t from "io-ts/lib/index.js"; +import { generateMnemonic } from "bip39"; + +export interface SeedPhraseBrand { + readonly SeedPhrase: unique symbol; +} + +export const SeedPhraseGuard = t.brand( + t.array(t.string), + (s): s is t.Branded => true, + "SeedPhrase" +); + +export type SeedPhrase = t.TypeOf; + +export const seedPhrase = (s: string[]) => + unsafeEither(SeedPhraseGuard.decode(s)); + +export type SeedSize = "15-words" | "24-words"; + +export const generateSeedPhrase = (strength: SeedSize): SeedPhrase => { + switch (strength) { + case "15-words": + return seedPhrase(generateMnemonic(160).split(" ")); + case "24-words": + return seedPhrase(generateMnemonic(256).split(" ")); + } +}; diff --git a/packages/testing-kit/typedoc.json b/packages/testing-kit/typedoc.json new file mode 100644 index 00000000..103a6fb2 --- /dev/null +++ b/packages/testing-kit/typedoc.json @@ -0,0 +1,5 @@ +{ + "entryPointStrategy": "expand", + "entryPoints": ["./src"], + "tsconfig": "./src/tsconfig.json" +} diff --git a/packages/wallet/package.json b/packages/wallet/package.json index d2ed6599..e570a82f 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -44,11 +44,6 @@ "import": "./dist/esm/lucid/index.js", "require": "./dist/bundled/cjs/lucid.cjs", "types": "./dist/esm/lucid/index.d.ts" - }, - "./nodejs": { - "import": "./dist/esm/nodejs/index.js", - "require": "./dist/bundled/cjs/nodejs.cjs", - "types": "./dist/esm/nodejs/index.d.ts" } }, "dependencies": { diff --git a/packages/wallet/src/lucid/index.ts b/packages/wallet/src/lucid/index.ts index 1cd87f64..25dce435 100644 --- a/packages/wallet/src/lucid/index.ts +++ b/packages/wallet/src/lucid/index.ts @@ -31,7 +31,7 @@ import * as Codec from "@47ng/codec"; import { pipe } from "fp-ts/lib/function.js"; import * as A from "fp-ts/lib/Array.js"; import * as R from "fp-ts/lib/Record.js"; -import { Monoid } from "fp-ts/lib/Monoid.js"; +import { mergeAssets } from "@marlowe.io/adapter/lucid"; import { addressBech32, MarloweTxCBORHex, @@ -44,20 +44,6 @@ const getAssetName: (unit: Unit) => string = (unit) => { return assetName ? Codec.hexToUTF8(assetName) : ""; }; -// Function that tells how to join two assets -const addAssets = { concat: (x: bigint, y: bigint) => x + y }; - -// A monoid for Lucid's Assets indicates how to create -// an empty Assets object and how to merge two Assets objects. -const mergeAssets: Monoid = { - // Lucid's Assets object are a Record, - // so the empty assets is the empty object - empty: {}, - // And to join two assets we join the two records. When - // the "assetId" is the same, the quantities are added. - concat: (x, y) => R.union(addAssets)(x)(y), -}; - const getAddress = ({ lucid }: LucidDI) => () => diff --git a/packages/wallet/src/nodejs/index.ts b/packages/wallet/src/nodejs/index.ts deleted file mode 100644 index b46e3c81..00000000 --- a/packages/wallet/src/nodejs/index.ts +++ /dev/null @@ -1,340 +0,0 @@ -import * as API from "@blockfrost/blockfrost-js"; -import { - Blockfrost, - Lucid, - Network, - C, - PrivateKey, - PolicyId, - getAddressDetails, - toUnit, - fromText, - NativeScript, - Tx, - TxSigned, - TxComplete, - Script, - fromHex, - toHex, - fromUnit, - Unit, -} from "lucid-cardano"; -import * as A from "fp-ts/lib/Array.js"; -import { pipe } from "fp-ts/lib/function.js"; -import * as O from "fp-ts/lib/Option.js"; -import * as TE from "fp-ts/lib/TaskEither.js"; -import * as T from "fp-ts/lib/Task.js"; - -import { unsafeTaskEither } from "@marlowe.io/adapter/fp-ts"; -import { - AddressBech32, - TxOutRef, - addressBech32, - MarloweTxCBORHex, - Token, - lovelaces, - token, - assetId, - policyId, - AssetId, -} from "@marlowe.io/runtime-core"; -import * as RuntimeCore from "@marlowe.io/runtime-core"; -import { WalletAPI } from "../api.js"; -import * as Codec from "@47ng/codec"; -import { MarloweJSON } from "@marlowe.io/adapter/codec"; -const log = (message: string) => console.log(`\t## - ${message}`); - -// TODO: Make nominal -export type PrivateKeysAsHex = string; -export type Address = string; - -// TODO: This is a pure datatype, convert to type alias or interface -export class Context { - projectId: string; - network: RuntimeCore.Network; - blockfrostUrl: string; - - public constructor( - projectId: string, - blockfrostUrl: string, - network: RuntimeCore.Network - ) { - this.projectId = projectId; - this.network = network; - this.blockfrostUrl = blockfrostUrl; - } - - public toLucidNetwork(): Network { - switch (this.network) { - case "private": - return "Custom"; - case "preview": - return "Preview"; - case "preprod": - return "Preprod"; - case "mainnet": - return "Mainnet"; - } - } -} - -// [[testing-wallet-discussion]] -// DISCUSSION: Currently this class is more of a testing helper rather than being a NodeJS -// implementation of the WalletAPI. It has extra methods for funding a wallet -// and minting test tokens and it is missing some required methods like getUTxOs. -// -// If we want to support a NodeJS implementation of the WalletAPI we should -// probably remove the extra methods and find a way to share the Blockfrost -// (or eventual underlying service) for testing. -// -// It we don't want to support a NodeJS library for the moment, then this could -// be moved to a @marlowe.io/runtime-xxx package, as it is not helping test the -// wallet, but the runtime. -/** - * @hidden - */ -export class SingleAddressWallet implements WalletAPI { - private privateKeyBech32: string; - private context: Context; - private lucid: Lucid; - private blockfrostApi: API.BlockFrostAPI; - - public address: AddressBech32; - getChangeAddress: T.Task; - getUsedAddresses: T.Task; - getCollaterals: T.Task; - - private constructor(context: Context, privateKeyBech32: PrivateKey) { - this.privateKeyBech32 = privateKeyBech32; - this.context = context; - this.blockfrostApi = new API.BlockFrostAPI({ - projectId: context.projectId, - }); - } - - // TODO: Extract this to its own function - static async Initialise( - context: Context, - privateKeyBech32: string - ): Promise { - const account = new SingleAddressWallet(context, privateKeyBech32); - await account.initialise(); - return account; - } - - // TODO: Extract this to its own function - static async Random(context: Context): Promise { - const privateKey = C.PrivateKey.generate_ed25519().to_bech32(); - const account = new SingleAddressWallet(context, privateKey); - await account.initialise(); - return account; - } - - private async initialise() { - this.lucid = await Lucid.new( - new Blockfrost(this.context.blockfrostUrl, this.context.projectId), - this.context.toLucidNetwork() - ); - this.lucid.selectWalletFromPrivateKey(this.privateKeyBech32); - this.address = addressBech32(await this.lucid.wallet.address()); - this.getChangeAddress = T.of(this.address); - this.getUsedAddresses = T.of([this.address]); - this.getCollaterals = T.of([]); - } - - async isMainnet() { - return this.lucid.network === "Mainnet"; - } - - async getTokens(): Promise { - try { - const content = await this.blockfrostApi.addresses(this.address); - return pipe( - content.amount ?? [], - A.map((tokenBlockfrost) => - tokenBlockfrost.unit === "lovelace" - ? lovelaces(BigInt(tokenBlockfrost.quantity)) - : token(BigInt(tokenBlockfrost.quantity).valueOf())( - assetId(policyId(fromUnit(tokenBlockfrost.unit).policyId))( - getAssetName(tokenBlockfrost.unit) - ) - ) - ) - ); - } catch (reason) { - throw new Error(`Error while retrieving assetBalance : ${reason}`); - } - } - - async getLovelaces(): Promise { - try { - const content = await this.blockfrostApi.addresses(this.address); - return pipe( - content.amount ?? [], - A.filter((amount) => amount.unit === "lovelaces"), - A.map((amount) => BigInt(amount.quantity)), - A.head, - O.getOrElse(() => 0n) - ); - } catch (reason) { - throw new Error(`Error while retrieving assetBalance : ${reason}`); - } - } - - public tokenBalance: (assetId: AssetId) => TE.TaskEither = ( - assetId - ) => - pipe( - TE.tryCatch( - () => this.blockfrostApi.addresses(this.address), - (reason) => new Error(`Error while retrieving assetBalance : ${reason}`) - ), - TE.map((content) => - pipe( - content.amount ?? [], - A.filter( - (amount) => - amount.unit === - toUnit(assetId.policyId, fromText(assetId.assetName)) - ), - A.map((amount) => BigInt(amount.quantity)), - A.head, - O.getOrElse(() => 0n) - ) - ) - ); - - // see [[testing-wallet-discussion]] - public provision: ( - provisionning: [SingleAddressWallet, bigint][] - ) => TE.TaskEither = (provisionning) => - pipe( - provisionning, - A.reduce( - this.lucid.newTx(), - (tx: Tx, account: [SingleAddressWallet, bigint]) => - tx.payToAddress(account[0].address, { - lovelace: account[1], - }) - ), - build, - TE.chain(this.signSubmitAndWaitConfirmation) - ); - - // see [[testing-wallet-discussion]] - public async randomPolicyId(): Promise<[Script, PolicyId]> { - const { paymentCredential } = this.lucid.utils.getAddressDetails( - await this.lucid.wallet.address() - ); - - const json: NativeScript = { - type: "all", - scripts: [ - { - type: "before", - slot: this.lucid.utils.unixTimeToSlot(Date.now() + 1000000), - }, - { type: "sig", keyHash: paymentCredential?.hash! }, - ], - }; - const script = this.lucid.utils.nativeScriptFromJson(json); - const policyId = this.lucid.utils.mintingPolicyToId(script); - return [script, policyId]; - } - - // see [[testing-wallet-discussion]] - public async mintRandomTokens( - assetName: string, - amount: bigint - ): Promise { - const policyRefs = await this.randomPolicyId(); - const [mintingPolicy, aPolicyId] = policyRefs; - const assets = { - [toUnit(aPolicyId, fromText(assetName))]: amount.valueOf(), - }; - - return unsafeTaskEither( - pipe( - this.lucid - .newTx() - .mintAssets(assets) - .validTo(Date.now() + 100000) - .attachMintingPolicy(mintingPolicy), - build, - TE.chain(this.signSubmitAndWaitConfirmation), - TE.map(() => token(amount)(assetId(policyId(aPolicyId))(assetName))) - ) - ); - } - async signTx(cborHex: MarloweTxCBORHex) { - const tx = C.Transaction.from_bytes(fromHex(cborHex)); - try { - const txSigned = await this.lucid.wallet.signTx(tx); - return toHex(txSigned.to_bytes()); - } catch (reason) { - throw 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}`) - ); - - waitConfirmation(txHash: string) { - try { - return this.lucid.awaitTx(txHash); - } catch (reason) { - throw new Error(`Error while awiting : ${reason}`); - } - } - // see [[testing-wallet-discussion]] - 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((txHash) => - TE.tryCatch( - () => this.waitConfirmation(txHash), - (reason) => - new Error(`Error while retrieving assetBalance : ${reason}`) - ) - ) - ); - // FIXME: Implement - // see [[testing-wallet-discussion]] - public getUTxOs: T.Task = T.of([]); -} - -const build = (tx: Tx): TE.TaskEither => - TE.tryCatch( - () => tx.complete(), - (reason) => new Error(`Error while building Tx : ${reason}`) - ); - -const getAssetName: (unit: Unit) => string = (unit) => { - const assetName = fromUnit(unit).assetName; - return assetName ? Codec.hexToUTF8(assetName) : ""; -}; - -/** - * Currently used for testing - * see [[testing-wallet-discussion]] - * @hidden - */ -export const getPrivateKeyFromHexString = (privateKeyHex: string): PrivateKey => - C.PrivateKey.from_bytes(Buffer.from(privateKeyHex, "hex")).to_bech32(); diff --git a/packages/wallet/test/wallet.spec.ts b/packages/wallet/test/wallet.spec.ts index 90d8fc0f..c989b2c0 100644 --- a/packages/wallet/test/wallet.spec.ts +++ b/packages/wallet/test/wallet.spec.ts @@ -1,7 +1,6 @@ -import { getPrivateKeyFromHexString } from "@marlowe.io/wallet/nodejs"; describe("wallet", () => { it("succeeds", () => { - expect(getPrivateKeyFromHexString).toBeDefined(); + expect(true).toBeDefined(); }); }); diff --git a/tsconfig-base.json b/tsconfig-base.json index 14ec5ef0..8d14d1f2 100644 --- a/tsconfig-base.json +++ b/tsconfig-base.json @@ -8,7 +8,7 @@ "inlineSourceMap": false, "lib": ["es2020", "dom"], "target": "ES2020", - "module": "ES2020", + "module": "Node16", "listEmittedFiles": false, "listFiles": false, "moduleResolution": "Node16", diff --git a/tsconfig.json b/tsconfig.json index a6cd9002..13fa66a2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ { "path": "./packages/runtime/core/src" }, { "path": "./packages/runtime/lifecycle/src" }, { "path": "./packages/token-metadata-client/src" }, - { "path": "./packages/marlowe-object/src" } + { "path": "./packages/marlowe-object/src" }, + { "path": "./packages/testing-kit/src" } ] }