Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Initial Mobile support (ussing cip45) #179

Merged
merged 4 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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))

4 changes: 4 additions & 0 deletions changelog.d/20240122_133315_hrajchert_cip45.md
Original file line number Diff line number Diff line change
@@ -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))
18 changes: 18 additions & 0 deletions examples/cip45-flow/Readme.md
Original file line number Diff line number Diff line change
@@ -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.
41 changes: 41 additions & 0 deletions examples/cip45-flow/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CIP45 test Flow</title>
<!-- This import map tells the browser what version of the SDK to use, the route is rewritten by the
local web server to point to the correct version -->
<script src="/importmap"></script>
</head>
<body>
<h1>CIP45 test Flow</h1>
<div>
<h2>Setup Runtime</h2>

<label for="runtimeUrl">URL to a Marlowe Runtime instance:</label>
<input
id="runtimeUrl"
type="text"
autocomplete="on"
placeholder="http://localhost:32952"
/>
</div>
<div id="qr-code"></div>
<div id="connected-wallet" style="display: none">
<h2>Connected Wallet</h2>
<div id="connected-wallet-address"></div>
<div id="connected-wallet-balance"></div>
<button id="create-contract">Create contract</button>
<button id="wallet-flow">Wallet flow</button>
<button id="disconnect-wallet">Disconnect Wallet</button>
</div>
<span id="connection-id"></span>
<h2>Console</h2>
<input id="clear-console" type="button" value="Clear console" />
<div id="console"></div>

<script src="https://fabianbormann.github.io/cardano-peer-connect/latest/index.js"></script>
<script type="module" src="./index.js"></script>
</body>
</html>
170 changes: 170 additions & 0 deletions examples/cip45-flow/index.js
Original file line number Diff line number Diff line change
@@ -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(`<h2>CIP-45 Wallet Extension</h2>`);
log("");
log("Reading Wallet information...");
log("");

const isMainnet = await wallet.isMainnet();
log(`* <b>Network</b>: ${isMainnet ? "Mainnet" : "Testnet"}`);
log("");

const lovelaces = await wallet.getLovelaces();
log("- <b>Lovelaces</b>: " + lovelaces);
const tokensResult = await wallet.getTokens();
log("");

log(`- <b>Tokens</b>: (${tokensResult.length} tokens)`);
tokensResult.map((token) => {
const tokenName =
token.assetId.assetName == "" ? "lovelaces" : token.assetId.assetName;
log(`&nbsp;&nbsp;&nbsp; <i>${tokenName}</i> - ${token.quantity}`);
});
log("");

const changeAddress = await wallet.getChangeAddress();
log("- <b>Change Address</b>: " + changeAddress);
log("");

const usedAddresses = await wallet.getUsedAddresses();
log(`- <b>Used Addresses</b>: (${usedAddresses.length} addresses)`);
usedAddresses.map((usedAddress) =>
log("&nbsp;&nbsp;&nbsp; - " + usedAddress)
);
log("");

const collaterals = await wallet.getCollaterals();
log(`- <b>Collaterals</b>: (${collaterals.length} collaterals)`);
collaterals.map((collateral) => log("&nbsp;&nbsp;&nbsp; - " + collateral));
log("");

const utxos = await wallet.getUTxOs();
log(`- <b>UTxOs</b>: (${utxos.length} utxos)`);
utxos.map((utxo) => log("&nbsp;&nbsp;&nbsp; - " + 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"));
2 changes: 2 additions & 0 deletions jsdelivr-npm-importmap.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ const importMap = {
"https://cdn.jsdelivr.net/npm/@marlowe.io/[email protected]/dist/bundled/esm/browser.js",
"@marlowe.io/wallet/lucid":
"https://cdn.jsdelivr.net/npm/@marlowe.io/[email protected]/dist/bundled/esm/lucid.js",
"@marlowe.io/wallet/peer-connect":
"https://cdn.jsdelivr.net/npm/@marlowe.io/[email protected]/dist/bundled/esm/peer-connect.js",
"@marlowe.io/runtime-rest-client":
"https://cdn.jsdelivr.net/npm/@marlowe.io/[email protected]/dist/bundled/esm/runtime-rest-client.js",
"@marlowe.io/runtime-rest-client/contract":
Expand Down
11 changes: 8 additions & 3 deletions packages/wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
45 changes: 36 additions & 9 deletions packages/wallet/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>((txConfirm) => {
Expand All @@ -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();
Expand Down
Loading