diff --git a/changelog.d/20240112_130130_hrajchert_plt_9089_createContract_bundle_support.md b/changelog.d/20240112_130130_hrajchert_plt_9089_createContract_bundle_support.md index 4ad21d9c..20252d9c 100644 --- a/changelog.d/20240112_130130_hrajchert_plt_9089_createContract_bundle_support.md +++ b/changelog.d/20240112_130130_hrajchert_plt_9089_createContract_bundle_support.md @@ -1,5 +1,5 @@ ### @marlowe.io/runtime-lifecycle -- Added support for contract bundles in the `lifecycle.contracts.createContract` function. ([PR-167](https://github.com/input-output-hk/marlowe-ts-sdk/pull/167)) +- Feat (PLT-9089): Added support for contract bundles in the `lifecycle.contracts.createContract` function. ([PR-167](https://github.com/input-output-hk/marlowe-ts-sdk/pull/167)) diff --git a/changelog.d/20240122_133315_hrajchert_cip45.md b/changelog.d/20240122_133315_hrajchert_cip45.md new file mode 100644 index 00000000..8d2ceae8 --- /dev/null +++ b/changelog.d/20240122_133315_hrajchert_cip45.md @@ -0,0 +1,4 @@ + +### @marlowe.io/wallet + +- Feat: Added a `@marlowe.io/wallet/peer-connect` module to enable mobile support by adapting to the [cardano-peer-connect](https://github.com/fabianbormann/cardano-peer-connect) library. ([PR-179](https://github.com/input-output-hk/marlowe-ts-sdk/pull/179)) diff --git a/examples/cip45-flow/Readme.md b/examples/cip45-flow/Readme.md new file mode 100644 index 00000000..59f7228c --- /dev/null +++ b/examples/cip45-flow/Readme.md @@ -0,0 +1,18 @@ +# CIP-45 flow + +This example is a Work In Progress. It shows how to use the `@marlowe.io/wallet/peer-connect` module together with the [cardano-peer-connect](https://github.com/fabianbormann/cardano-peer-connect) library. + +## How to run + +1. Build the project +```bash +$ npm i +$ npm run build +``` +2. Serve the examples folder +```bash +$ npm run serve-dev +``` +3. Open the browser at http://localhost:1337/examples/cip45-flow/ +4. Use a cip45 wallet like [Eternl](https://eternl.io/) and scan the QR code to connect to the wallet +5. After the prompt to connect, you can click the create contract button. diff --git a/examples/cip45-flow/index.html b/examples/cip45-flow/index.html new file mode 100644 index 00000000..4e0a6d8c --- /dev/null +++ b/examples/cip45-flow/index.html @@ -0,0 +1,41 @@ + + + + + + CIP45 test Flow + + + + +

CIP45 test Flow

+
+

Setup Runtime

+ + + +
+
+ + +

Console

+ +
+ + + + + diff --git a/examples/cip45-flow/index.js b/examples/cip45-flow/index.js new file mode 100644 index 00000000..08a5fd05 --- /dev/null +++ b/examples/cip45-flow/index.js @@ -0,0 +1,170 @@ +import { mkRestClient } from "@marlowe.io/runtime-rest-client"; +import { MarloweJSON } from "@marlowe.io/adapter/codec"; +import { clearConsole, log, logJSON } from "../js/poc-helpers.js"; +import { mkPeerConnectAdapter } from "@marlowe.io/wallet/peer-connect"; +import { mkRuntimeLifecycle } from "@marlowe.io/runtime-lifecycle"; + +import * as H from "../js/poc-helpers.js"; + +window.restClient = null; +function getRestClient() { + if (window.restClient === null) { + const runtimeURL = H.getRuntimeUrl(); + window.restClient = mkRestClient(runtimeURL); + } + return window.restClient; +} +const runtimeUrlInput = document.getElementById("runtimeUrl"); +runtimeUrlInput.addEventListener("change", () => { + window.restClient = null; +}); + +const clearConsoleButton = document.getElementById("clear-console"); +clearConsoleButton.addEventListener("click", clearConsole); + +H.setupLocalStorageRuntimeUrl(); + +const adapter = mkPeerConnectAdapter(); +adapter.onDeleteWallet(async (walletId) => { + updateDisconnectedWalletStatus(); +}); +adapter.onNewWallet(async (walletId, wallet) => { + updateConnectedWalletStatus(wallet); +}); +window.adapter = adapter; + +// Give your app some basic information that will be displayed to the client wallet when he is connecting to your DApp. +const dAppInfo = { + name: "Cip45 test", + url: "http://localhost:1337/examples", +}; +// Define a function that will be called when the client tries to connect to your DApp. +const verifyConnection = (walletInfo, callback) => { + logJSON("walletInfo", walletInfo); + callback( + // + window.confirm( + `Do you want to connect to wallet ${walletInfo.name} (${walletInfo.address})?` + ) + ); +}; + +function updateDisconnectedWalletStatus() { + document.getElementById("connected-wallet").style.display = "none"; + document.getElementById("qr-code").style.display = "block"; +} + +async function updateConnectedWalletStatus(wallet) { + const address = await wallet.getChangeAddress(); + const balance = await wallet.getLovelaces(); + const addressElm = document.getElementById("connected-wallet-address"); + const balanceElm = document.getElementById("connected-wallet-balance"); + addressElm.textContent = address; + balanceElm.textContent = balance; + + document.getElementById("connected-wallet").style.display = "block"; + document.getElementById("qr-code").style.display = "none"; +} + +const dAppConnect = new CardanoPeerConnect.DAppPeerConnect({ + dAppInfo: dAppInfo, + verifyConnection: verifyConnection, + onApiInject: adapter.adaptApiInject, + onApiEject: adapter.adaptApiEject, +}); +window.dAppConnect = dAppConnect; + +document + .getElementById("disconnect-wallet") + .addEventListener("click", async () => { + // NOTE: dAppConnect doesn't have a disconnect method, and this is currently + // not working. It's not clear how to disconnect from the wallet. + // https://github.com/fabianbormann/cardano-peer-connect/issues/57 + logJSON("adapter", adapter); + logJSON("dAppConnect", dAppConnect); + const walletInfo = { version: 1, name: "a", icon: "bubu" }; + dAppConnect.meerkat.api.disconnect( + // dAppConnect.dAppInfo.address, + dAppConnect.connectedWallet, + walletInfo, + (connectStatus) => { + console.log(connectStatus); + debugger; + } + ); + }); + +document + .getElementById("create-contract") + .addEventListener("click", async () => { + const wallet = adapter.getWallet(); + const runtime = mkRuntimeLifecycle({ + runtimeURL: H.getRuntimeUrl(), + wallet, + }); + + const [contractId, txId] = await runtime.contracts.createContract({ + contract: "close", + tags: { + "cip-45": "true", + }, + }); + + log(`contractId: ${contractId}`); + log(`waiting for txId ${txId}`); + await wallet.waitConfirmation(txId); + log("transaction confirmed"); + }); + +document.getElementById("wallet-flow").addEventListener("click", async () => { + const wallet = adapter.getWallet(); + log(`

CIP-45 Wallet Extension

`); + log(""); + log("Reading Wallet information..."); + log(""); + + const isMainnet = await wallet.isMainnet(); + log(`* Network: ${isMainnet ? "Mainnet" : "Testnet"}`); + log(""); + + const lovelaces = await wallet.getLovelaces(); + log("- Lovelaces: " + lovelaces); + const tokensResult = await wallet.getTokens(); + log(""); + + log(`- Tokens: (${tokensResult.length} tokens)`); + tokensResult.map((token) => { + const tokenName = + token.assetId.assetName == "" ? "lovelaces" : token.assetId.assetName; + log(`    ${tokenName} - ${token.quantity}`); + }); + log(""); + + const changeAddress = await wallet.getChangeAddress(); + log("- Change Address: " + changeAddress); + log(""); + + const usedAddresses = await wallet.getUsedAddresses(); + log(`- Used Addresses: (${usedAddresses.length} addresses)`); + usedAddresses.map((usedAddress) => + log("    - " + usedAddress) + ); + log(""); + + const collaterals = await wallet.getCollaterals(); + log(`- Collaterals: (${collaterals.length} collaterals)`); + collaterals.map((collateral) => log("    - " + collateral)); + log(""); + + const utxos = await wallet.getUTxOs(); + log(`- UTxOs: (${utxos.length} utxos)`); + utxos.map((utxo) => log("    - " + utxo)); + log(""); + log("Wallet flow done 🎉"); +}); +// This is the code (identifier) that the client needs to enter into the wallet to connect to your dapp +const clientConnectCode = dAppConnect.getAddress(); +document.getElementById("connection-id").innerText = clientConnectCode; + +// Create and insert a QR code on your DApp, so the user can scan it easily in their app +dAppConnect.generateQRCode(document.getElementById("qr-code")); diff --git a/jsdelivr-npm-importmap.js b/jsdelivr-npm-importmap.js index 2361c825..195f3e1f 100644 --- a/jsdelivr-npm-importmap.js +++ b/jsdelivr-npm-importmap.js @@ -48,6 +48,8 @@ const importMap = { "https://cdn.jsdelivr.net/npm/@marlowe.io/wallet@0.3.0-beta/dist/bundled/esm/browser.js", "@marlowe.io/wallet/lucid": "https://cdn.jsdelivr.net/npm/@marlowe.io/wallet@0.3.0-beta/dist/bundled/esm/lucid.js", + "@marlowe.io/wallet/peer-connect": + "https://cdn.jsdelivr.net/npm/@marlowe.io/wallet@0.3.0-beta/dist/bundled/esm/peer-connect.js", "@marlowe.io/runtime-rest-client": "https://cdn.jsdelivr.net/npm/@marlowe.io/runtime-rest-client@0.3.0-beta/dist/bundled/esm/runtime-rest-client.js", "@marlowe.io/runtime-rest-client/contract": diff --git a/packages/wallet/package.json b/packages/wallet/package.json index d5bcf2df..9aafd2c8 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -44,14 +44,19 @@ "import": "./dist/esm/lucid/index.js", "require": "./dist/bundled/cjs/lucid.cjs", "types": "./dist/esm/lucid/index.d.ts" + }, + "./peer-connect": { + "import": "./dist/esm/peer-connect/index.js", + "require": "./dist/bundled/cjs/peer-connect.cjs", + "types": "./dist/esm/peer-connect/index.d.ts" } }, "dependencies": { + "@47ng/codec": "1.1.0", + "@blockfrost/blockfrost-js": "^5.4.0", "fp-ts": "^2.16.1", "io-ts": "2.2.21", - "newtype-ts": "0.3.5", - "@47ng/codec": "1.1.0", "lucid-cardano": "0.10.7", - "@blockfrost/blockfrost-js": "^5.4.0" + "newtype-ts": "0.3.5" } } diff --git a/packages/wallet/src/browser/index.ts b/packages/wallet/src/browser/index.ts index e92bf962..1e927c00 100644 --- a/packages/wallet/src/browser/index.ts +++ b/packages/wallet/src/browser/index.ts @@ -101,7 +101,10 @@ export async function mkBrowserWallet( // DISCUSSION: This can currently wait forever. Maybe we should add // an abort controller or a timeout -const waitConfirmation = +/** + * @hidden + */ +export const waitConfirmation = (di: ExtensionDI) => (txHash: string, checkInterval = 3000) => { return new Promise((txConfirm) => { @@ -118,55 +121,79 @@ const waitConfirmation = }); }; -const signTx = +/** + * @hidden + */ +export const signTx = ({ extension }: ExtensionDI) => (tx: string) => { return extension.signTx(tx, true); }; -const getChangeAddress = +/** + * @hidden + */ +export const getChangeAddress = ({ extension }: ExtensionDI) => async () => { const changeAddress = await extension.getChangeAddress(); return deserializeAddress(changeAddress); }; -const getUsedAddresses = +/** + * @hidden + */ +export const getUsedAddresses = ({ extension }: ExtensionDI) => async () => { const usedAddresses = await extension.getUsedAddresses(); return usedAddresses.map(deserializeAddress); }; -const getUTxOs = +/** + * @hidden + */ +export const getUTxOs = ({ extension }: ExtensionDI) => async () => { const utxos = (await extension.getUtxos()) ?? []; return utxos.map(deserializeTxOutRef); }; -const getCollaterals = +/** + * @hidden + */ +export const getCollaterals = ({ extension }: ExtensionDI) => async () => { const collaterals = (await extension.experimental.getCollateral()) ?? []; return collaterals.map(deserializeTxOutRef); }; -const isMainnet = +/** + * @hidden + */ +export const isMainnet = ({ extension }: ExtensionDI) => async () => { const networkId = await extension.getNetworkId(); return networkId == 1; }; -const getTokens = +/** + * @hidden + */ +export const getTokens = ({ extension }: ExtensionDI) => async () => { const balances = await extension.getBalance(); return valueToTokens(deserializeValue(balances)); }; -const getLovelaces = +/** + * @hidden + */ +export const getLovelaces = ({ extension }: ExtensionDI) => async () => { const balances = await extension.getBalance(); diff --git a/packages/wallet/src/peer-connect/index.ts b/packages/wallet/src/peer-connect/index.ts new file mode 100644 index 00000000..0c4c36fd --- /dev/null +++ b/packages/wallet/src/peer-connect/index.ts @@ -0,0 +1,44 @@ +import { WalletAPI } from "../api.js"; +import * as Browser from "../browser/index.js"; + +type WalletHandler = (walletId: string, wallet: WalletAPI) => void; +export const mkPeerConnectAdapter = () => { + let wallet: WalletAPI | undefined; + let newWalletHandler: WalletHandler = () => {}; + let deleteWalletHandler: WalletHandler = () => {}; + return { + adaptApiEject(walletName: string, walletId: string) { + if (wallet) { + deleteWalletHandler(walletId, wallet); + } + wallet = undefined; + }, + async adaptApiInject(walletName: string, walletId: string) { + const di = { + extension: await window.cardano[walletName.toLowerCase()].enable(), + }; + const peerWallet = { + waitConfirmation: Browser.waitConfirmation(di), + signTx: Browser.signTx(di), + getChangeAddress: Browser.getChangeAddress(di), + getUsedAddresses: Browser.getUsedAddresses(di), + getCollaterals: Browser.getCollaterals(di), + getUTxOs: Browser.getUTxOs(di), + isMainnet: Browser.isMainnet(di), + getTokens: Browser.getTokens(di), + getLovelaces: Browser.getLovelaces(di), + }; + wallet = peerWallet; + newWalletHandler(walletId, peerWallet); + }, + onNewWallet(handler: WalletHandler) { + newWalletHandler = handler; + }, + onDeleteWallet(handler: WalletHandler) { + deleteWalletHandler = handler; + }, + getWallet() { + return wallet; + }, + }; +};