Skip to content

Commit 1007620

Browse files
committed
Add JS wrapper and HTML example for wasm API
1 parent 69b1574 commit 1007620

File tree

6 files changed

+302
-1
lines changed

6 files changed

+302
-1
lines changed

cardano-wasm/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,32 @@ That will create it with the name `cardano-wasm.js` in the current folder.
203203

204204
You can find more information in [this url](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/wasm.html).
205205

206+
And you can find an example of how to use it in the `example` subfolder. This example assumes that the generated `.wasm` and `.js` files reside in the same folder as the code in `example` subfolder.
207+
208+
## Running the example
209+
210+
To run the example in the `example` subfolder:
211+
212+
1. Copy the generated `cardano-wasm.wasm` file to the `example` subfolder. You can find its location using:
213+
```console
214+
echo "$(env -u CABAL_CONFIG wasm32-wasi-cabal list-bin exe:cardano-wasm | tail -n1)"
215+
```
216+
2. Copy the generated `cardano-wasm.js` file (generated by the `post-link.mjs` command in the section above) to the `example` subfolder.
217+
3. Navigate to the `example` subfolder in your terminal.
218+
4. Due to browser security restrictions (CORS policy), you may need to serve the `index.html` file through a local HTTP server. A simple way to do this is using Python's built-in HTTP server:
219+
```console
220+
python3 -m http.server 8001
221+
```
222+
5. Open your web browser and navigate to `http://localhost:8001/`. You should see a blank page, and if you open the developer console you should be able to see an output like the following:
223+
```console
224+
[Log] wasi: – 0 – 0 (wasi.js, line 1)
225+
[Log] wasi: – 0 – 0 (wasi.js, line 1)
226+
[Log] Tx input: (test, line 9)
227+
[Log] be6efd42a3d7b9a00d09d77a5d41e55ceaf0bd093a8aa8a893ce70d9caafd978#0 (test, line 10)
228+
[Log] Tx body: (test, line 16)
229+
[Log] {cborHex: "84a300d9010281825820be6efd42a3d7b9a00d09d77a5d41e5…86b4e8a52c6555f659b871a00989680021a001e8480a0f5f6",
230+
description: "Ledger Cddl Format", type: "TxBodyConway"} (test, line 17)
231+
[Log] Signed tx: (test, line 19)
232+
[Log] {cborHex: "84a300d9010281825820be6efd42a3d7b9a00d09d77a5d41e5…5ca8d8561a45358a27b7f5d1f4f7ceb3fec2a8725f20df5f6",
233+
description: "Ledger Cddl Format", type: "Tx ConwayEra"} (test, line 20)
234+
```

cardano-wasm/cardano-wasm.cabal

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ executable cardano-wasm
3737
ghc-options:
3838
-no-hs-main
3939
-optl-mexec-model=reactor
40-
"-optl-Wl,--strip-all,--export=newConwayTx,--export=addTxInput,--export=addSimpleTxOut,--export=setFee,--export=signWithPaymentKey,--export=alsoSignWithPaymentKey,--export=txToCbor"
40+
"-optl-Wl,--strip-all,--export=getApiInfo,--export=newConwayTx,--export=addTxInput,--export=addSimpleTxOut,--export=setFee,--export=signWithPaymentKey,--export=alsoSignWithPaymentKey,--export=txToCbor"
4141
other-modules:
42+
Cardano.Wasm.Internal.Api.Info
4243
Cardano.Wasm.Internal.Api.Tx
4344
Cardano.Wasm.Internal.ExceptionHandling
4445
Cardano.Wasm.Internal.JavaScript.Bridge
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { WASI } from "https://unpkg.com/@bjorn3/[email protected]/dist/index.js";
2+
import ghc_wasm_jsffi from "./cardano-wasm.js";
3+
const __exports = {};
4+
const wasi = new WASI([], [], []);
5+
async function initialize() {
6+
let { instance } = await WebAssembly.instantiateStreaming(fetch("./cardano-wasm.wasm"), {
7+
ghc_wasm_jsffi: ghc_wasm_jsffi(__exports),
8+
wasi_snapshot_preview1: wasi.wasiImport,
9+
})
10+
Object.assign(__exports, instance.exports);
11+
wasi.initialize(instance);
12+
13+
// Wrap a function with variable arguments to make the parameters inspectable
14+
function fixateArgs(params, func) {
15+
const paramString = params.join(',');
16+
// Dynamically create a function that captures 'func' from the closure.
17+
// 'this' and 'arguments' are passed through from the wrapper to 'func'.
18+
// Using eval allows the returned function to have named parameters for inspectability.
19+
const wrapper = eval(`
20+
(function(${paramString}) {
21+
return func.apply(this, arguments);
22+
})
23+
`);
24+
return wrapper;
25+
}
26+
27+
// Same as fixateArgs but for async functions
28+
async function fixateArgsAsync(params, func) {
29+
const paramString = params.join(',');
30+
// Dynamically create an async function.
31+
const wrapper = eval(`
32+
(async function(${paramString}) {
33+
return await func.apply(this, arguments);
34+
})
35+
`);
36+
return wrapper;
37+
}
38+
39+
// Dynamically build the API
40+
const apiInfo = await instance.exports.getApiInfo();
41+
let makers = {};
42+
let cardanoAPI = { objectType: "cardano-api" };
43+
// Create maker functions for each virtual object type
44+
apiInfo.virtualObjects.forEach(vo => {
45+
makers[vo.objectName] = function (initialHaskellValuePromise) {
46+
// currentHaskellValueProvider is a function that returns a Promise for the Haskell value
47+
// It starts with the initial value promise and fluent methods accumulate transformations
48+
let currentHaskellValueProvider = () => initialHaskellValuePromise;
49+
let jsObject = { objectType: vo.objectName };
50+
51+
vo.methods.forEach(method => {
52+
if (method.return.type === "fluent") {
53+
// Fluent methods are synchronous and update the provider
54+
// A fluent method is one that returns the same object type
55+
jsObject[method.name] = fixateArgs(method.params, function (...args) {
56+
const previousProvider = currentHaskellValueProvider;
57+
// We update the provider so that it resolves the previous provider and chains the next call
58+
currentHaskellValueProvider = async () => {
59+
const prevHaskellValue = await previousProvider();
60+
return instance.exports[method.name](prevHaskellValue, ...args);
61+
};
62+
return jsObject; // Return current object for supporting chaining
63+
});
64+
} else {
65+
// Non-fluent methods (newObject or other) are async and apply the accumulated method calls
66+
jsObject[method.name] = fixateArgs(method.params, async function (...args) {
67+
const haskellValue = await currentHaskellValueProvider(); // Resolve accumulated method calls
68+
const resultPromise = instance.exports[method.name](haskellValue, ...args); // Call the non-fluent method
69+
70+
if (method.return.type === "newObject") { // It returns a new object
71+
return makers[method.return.objectType](resultPromise);
72+
} else { // It returns some primitive or other JS type (not a virtual object)
73+
return resultPromise;
74+
}
75+
});
76+
}
77+
});
78+
return jsObject;
79+
};
80+
});
81+
82+
// Populate the main API object with static methods
83+
apiInfo.staticMethods.forEach(method => {
84+
cardanoAPI[method.name] = async function (...args) {
85+
const resultPromise = instance.exports[method.name](...args);
86+
87+
if (method.return.type === "newObject") { // Create a new object
88+
return makers[method.return.objectType](resultPromise);
89+
} else { // Return some primitive or other JS type (not a virtual object)
90+
return resultPromise;
91+
}
92+
};
93+
});
94+
return cardanoAPI;
95+
}
96+
export default initialize;

cardano-wasm/example/index.html

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<html>
2+
<body>
3+
<script type="module">
4+
import cardano_api from "./cardano-api.js";
5+
let promise = cardano_api();
6+
async function do_async_work() {
7+
let api = await promise;
8+
console.log("Api object:");
9+
console.log(api);
10+
11+
let emptyTx = await api.newConwayTx();
12+
console.log("UnsignedTx object:");
13+
console.log(emptyTx);
14+
15+
let signedTx = await emptyTx.addTxInput("be6efd42a3d7b9a00d09d77a5d41e55ceaf0bd093a8aa8a893ce70d9caafd978", 0)
16+
.addSimpleTxOut("addr_test1vzpfxhjyjdlgk5c0xt8xw26avqxs52rtf69993j4tajehpcue4v2v", 10_000_000n)
17+
.setFee(2_000_000n)
18+
.signWithPaymentKey("addr_sk1648253w4tf6fv5fk28dc7crsjsaw7d9ymhztd4favg3cwkhz7x8sl5u3ms");
19+
20+
console.log("SignedTx object:");
21+
console.log(signedTx);
22+
23+
let txCbor = await signedTx.txToCbor();
24+
console.log("Tx CBOR:");
25+
console.log(txCbor);
26+
}
27+
do_async_work().then(() => {});
28+
</script>
29+
</body>
30+
</html>
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
{-# LANGUAGE InstanceSigs #-}
2+
3+
module Cardano.Wasm.Internal.Api.Info (apiInfo) where
4+
5+
import Data.Aeson qualified as Aeson
6+
import Data.Text qualified as Text
7+
8+
-- * API Information Data Types
9+
10+
-- | Describes the return type of a method.
11+
data MethodReturnTypeInfo
12+
= -- | Returns an instance of the same object type (fluent interface).
13+
Fluent
14+
| -- | Returns a new instance of a specified virtual object type.
15+
NewObject String
16+
| -- | Returns a non-virtual-object type (e.g., JSString, number).
17+
OtherType String
18+
deriving (Show, Eq)
19+
20+
instance Aeson.ToJSON MethodReturnTypeInfo where
21+
toJSON Fluent = Aeson.object ["type" Aeson..= Text.pack "fluent"]
22+
toJSON (NewObject objTypeName) = Aeson.object ["type" Aeson..= Text.pack "newObject", "objectType" Aeson..= objTypeName]
23+
toJSON (OtherType typeName) = Aeson.object ["type" Aeson..= Text.pack "other", "typeName" Aeson..= typeName]
24+
25+
-- | Information about a single method of a virtual object.
26+
data MethodInfo = MethodInfo
27+
{ methodName :: String
28+
, methodParams :: [String]
29+
-- ^ Names of parameters, excluding 'this'.
30+
, methodReturnType :: MethodReturnTypeInfo
31+
}
32+
deriving (Show, Eq)
33+
34+
instance Aeson.ToJSON MethodInfo where
35+
toJSON :: MethodInfo -> Aeson.Value
36+
toJSON (MethodInfo name params retType) =
37+
Aeson.object
38+
[ "name" Aeson..= name
39+
, "params" Aeson..= params
40+
, "return" Aeson..= retType
41+
]
42+
43+
-- | Information about a virtual object and its methods.
44+
data VirtualObjectInfo = VirtualObjectInfo
45+
{ virtualObjectName :: String
46+
, virtualObjectMethods :: [MethodInfo]
47+
}
48+
deriving (Show, Eq)
49+
50+
instance Aeson.ToJSON VirtualObjectInfo where
51+
toJSON :: VirtualObjectInfo -> Aeson.Value
52+
toJSON (VirtualObjectInfo name methods) =
53+
Aeson.object
54+
[ "objectName" Aeson..= name
55+
, "methods" Aeson..= methods
56+
]
57+
58+
-- | Aggregate type for all API information.
59+
data ApiInfo = ApiInfo
60+
{ staticMethods :: [MethodInfo]
61+
, virtualObjects :: [VirtualObjectInfo]
62+
}
63+
deriving (Show, Eq)
64+
65+
instance Aeson.ToJSON ApiInfo where
66+
toJSON :: ApiInfo -> Aeson.Value
67+
toJSON (ApiInfo staticObjs virtualObjs) =
68+
Aeson.object
69+
[ "staticMethods" Aeson..= staticObjs
70+
, "virtualObjects" Aeson..= virtualObjs
71+
]
72+
73+
-- | Provides metadata about the "virtual objects" and their methods.
74+
-- This is intended to help generate JavaScript wrappers.
75+
apiInfo :: ApiInfo
76+
apiInfo =
77+
let unsignedTxObjectName = "UnsignedTx"
78+
signedTxObjectName = "SignedTx"
79+
80+
staticApiMethods =
81+
[ MethodInfo
82+
{ methodName = "newConwayTx"
83+
, methodParams = []
84+
, methodReturnType = NewObject unsignedTxObjectName
85+
}
86+
]
87+
88+
unsignedTxObj =
89+
VirtualObjectInfo
90+
{ virtualObjectName = unsignedTxObjectName
91+
, virtualObjectMethods =
92+
[ MethodInfo
93+
{ methodName = "addTxInput"
94+
, methodParams = ["txId", "txIx"]
95+
, methodReturnType = Fluent
96+
}
97+
, MethodInfo
98+
{ methodName = "addSimpleTxOut"
99+
, methodParams = ["destAddr", "lovelaceAmount"]
100+
, methodReturnType = Fluent
101+
}
102+
, MethodInfo
103+
{ methodName = "setFee"
104+
, methodParams = ["lovelaceAmount"]
105+
, methodReturnType = Fluent
106+
}
107+
, MethodInfo
108+
{ methodName = "signWithPaymentKey"
109+
, methodParams = ["signingKey"]
110+
, methodReturnType = NewObject signedTxObjectName
111+
}
112+
]
113+
}
114+
115+
signedTxObj =
116+
VirtualObjectInfo
117+
{ virtualObjectName = signedTxObjectName
118+
, virtualObjectMethods =
119+
[ MethodInfo
120+
{ methodName = "alsoSignWithPaymentKey"
121+
, methodParams = ["signingKey"]
122+
, methodReturnType = Fluent
123+
}
124+
, MethodInfo
125+
{ methodName = "txToCbor"
126+
, methodParams = []
127+
, methodReturnType = OtherType "string"
128+
}
129+
]
130+
}
131+
in ApiInfo
132+
{ staticMethods = staticApiMethods
133+
, virtualObjects = [unsignedTxObj, signedTxObj]
134+
}

cardano-wasm/src/Cardano/Wasm/Internal/JavaScript/Bridge.hs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module Cardano.Wasm.Internal.JavaScript.Bridge where
1616
import Cardano.Api qualified as Api
1717
import Cardano.Api.Ledger qualified as Ledger
1818

19+
import Cardano.Wasm.Internal.Api.Info (apiInfo)
1920
import Cardano.Wasm.Internal.Api.Tx qualified as Wasm
2021
import Cardano.Wasm.Internal.ExceptionHandling (rightOrError)
2122

@@ -88,6 +89,8 @@ jsValToType expectedType val = do
8889

8990
-- * Type Synonyms for JSVal representations
9091

92+
type JSApiInfo = JSVal
93+
9194
type JSUnsignedTx = JSVal
9295

9396
type JSSignedTx = JSVal
@@ -225,4 +228,12 @@ txToCbor :: JSSignedTx -> IO JSString
225228
txToCbor jsSignedTx =
226229
toJSVal . Wasm.toCborImpl =<< fromJSVal jsSignedTx
227230

231+
-- * API Information
232+
233+
foreign export javascript "getApiInfo"
234+
getApiInfo :: IO JSApiInfo
235+
236+
getApiInfo :: IO JSApiInfo
237+
getApiInfo = toJSVal apiInfo
238+
228239
#endif

0 commit comments

Comments
 (0)