Skip to content

Commit 6a1001a

Browse files
authored
Merge pull request #912 from IntersectMBO/grpc-support
Add wasm API support for querying era from cardano-node (through GRPC-web)
2 parents ee5af3f + 33c791d commit 6a1001a

File tree

13 files changed

+377
-14
lines changed

13 files changed

+377
-14
lines changed

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,12 @@ cardano-tracer/cardano-tracer-test
7474

7575
# IntellIJ project folder
7676
.idea/
77+
cardano-wasm/grpc-example/cardano-wasm.wasm
78+
cardano-wasm/grpc-example/cardano-wasm.js
79+
cardano-wasm/grpc-example/cardano-api.d.ts
80+
cardano-wasm/grpc-example/cardano-api.js
81+
cardano-wasm/grpc-example/node_grpc_web_pb.js
82+
cardano-wasm/example/cardano-wasm.wasm
83+
cardano-wasm/example/cardano-wasm.js
84+
cardano-wasm/example/cardano-api.d.ts
85+
cardano-wasm/example/cardano-api.js

cardano-wasm/README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ To run the example in the `example` subfolder:
220220
```console
221221
python3 -m http.server 8001
222222
```
223-
6. 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+
6. Open your web browser and navigate to `http://localhost:8001/`. You should see a page titled `Test Output` with the results of the test, and if you open the developer console you should be able to see an output like the following:
224224
```console
225225
[Log] wasi: – 0 – 0 (wasi.js, line 1)
226226
[Log] wasi: – 0 – 0 (wasi.js, line 1)
@@ -233,3 +233,31 @@ To run the example in the `example` subfolder:
233233
[Log] {cborHex: "84a300d9010281825820be6efd42a3d7b9a00d09d77a5d41e5…5ca8d8561a45358a27b7f5d1f4f7ceb3fec2a8725f20df5f6",
234234
description: "Ledger Cddl Format", type: "Tx ConwayEra"} (test, line 20)
235235
```
236+
237+
## Running the GRPC example
238+
239+
To run the example in the `grpc-example` subfolder:
240+
241+
1. Run an instance of the `cardano-node` with the GRPC server enabled and put the socket file for the GRPC server in the root folder of this repo with the name `rpc.socket`. (You can put it somewhere else, but you will have to update the `envoy-conf.yaml` function later.)
242+
2. Generate the JS GRPC client bundle `node_grpc_web_pb.js` from the GRPC server proto files by either:
243+
- Running `nix build .#proto-js-bundle`. (This will generate it under the `result` folder.)
244+
- Or using `protoc`, `npm` and `browserify`:
245+
```
246+
protoc -I../../cardano-rpc/proto --js_out=import_style=commonjs,binary:./ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:. ../../cardano-rpc/proto/cardano/rpc/node.proto
247+
248+
npm install grpc-web
249+
250+
npm install google-protobuf
251+
252+
browserify --standalone grpc cardano/rpc/node_grpc_web_pb.js > node_grpc_web_pb.js
253+
```
254+
3. Copy the generated `node_grpc_web_pb.js` file to the `grpc-example` subfolder.
255+
4. Copy the generated `cardano-wasm.wasm` file to the `grpc-example` subfolder. You can find its location using:
256+
```console
257+
echo "$(env -u CABAL_CONFIG wasm32-wasi-cabal list-bin exe:cardano-wasm | tail -n1)"
258+
```
259+
5. Copy the generated `cardano-wasm.js` file (generated by the `post-link.mjs` command in the section above) to the `grpc-example` subfolder.
260+
6. Copy the wrapper files from the `lib-wrapper` folder into the `grpc-example` subfolder.
261+
7. Run `envoy -c envoy-conf.yaml` from the `grpc-example` subfolder.
262+
8. Open your web browser and navigate to `http://localhost:8080/`. You should see a page titled `Test Output` with the results of the test.
263+

cardano-wasm/cardano-wasm.cabal

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,17 @@ executable cardano-wasm
3737
ghc-options:
3838
-no-hs-main
3939
-optl-mexec-model=reactor
40-
"-optl-Wl,--strip-all,--export=getApiInfo,--export=newConwayTx,--export=addTxInput,--export=addSimpleTxOut,--export=setFee,--export=estimateMinFee,--export=signWithPaymentKey,--export=alsoSignWithPaymentKey,--export=txToCbor"
40+
"-optl-Wl,--strip-all,--export=getApiInfo,--export=newConwayTx,--export=addTxInput,--export=addSimpleTxOut,--export=setFee,--export=estimateMinFee,--export=signWithPaymentKey,--export=alsoSignWithPaymentKey,--export=txToCbor,--export=newGrpcConnection,--export=getEra"
4141
other-modules:
42+
Cardano.Wasm.Internal.Api.GRPC
4243
Cardano.Wasm.Internal.Api.Info
4344
Cardano.Wasm.Internal.Api.InfoToTypeScript
4445
Cardano.Wasm.Internal.Api.Tx
4546
Cardano.Wasm.Internal.Api.TypeScriptDefs
4647
Cardano.Wasm.Internal.ExceptionHandling
4748
Cardano.Wasm.Internal.JavaScript.Bridge
49+
Cardano.Wasm.Internal.JavaScript.GRPC
50+
Cardano.Wasm.Internal.JavaScript.GRPCTypes
4851

4952
build-depends:
5053
aeson,
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
static_resources:
2+
listeners:
3+
- name: main_listener
4+
address:
5+
socket_address:
6+
address: 0.0.0.0
7+
port_value: 8080
8+
filter_chains:
9+
- filters:
10+
- name: envoy.filters.network.http_connection_manager
11+
typed_config:
12+
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
13+
stat_prefix: ingress_http
14+
route_config:
15+
name: local_route
16+
max_direct_response_body_size_bytes: 104857600
17+
virtual_hosts:
18+
- name: local_service
19+
domains: ["*"]
20+
routes:
21+
# ROUTE 1: Match gRPC-Web requests based on their content-type.
22+
# This route is checked first.
23+
- match:
24+
prefix: "/"
25+
headers:
26+
- name: "Content-Type"
27+
string_match:
28+
exact: "application/grpc-web-text"
29+
route:
30+
cluster: grpc_service_unix_socket
31+
timeout: 60s
32+
33+
# ROUTE 2: Fallback for all other requests (e.g., your browser loading the page).
34+
# This route is checked only if the first one doesn't match.
35+
- match:
36+
path: "/"
37+
response_headers_to_add:
38+
- header:
39+
key: "Content-Type"
40+
value: "text/html"
41+
direct_response:
42+
status: 200
43+
body:
44+
filename: "./index.html"
45+
- match:
46+
path: "/index.html"
47+
response_headers_to_add:
48+
- header:
49+
key: "Content-Type"
50+
value: "text/html"
51+
direct_response:
52+
status: 200
53+
body:
54+
filename: "./index.html"
55+
- match:
56+
path: "/cardano-api.js"
57+
response_headers_to_add:
58+
- header:
59+
key: "Content-Type"
60+
value: "text/javascript"
61+
direct_response:
62+
status: 200
63+
body:
64+
filename: "./cardano-api.js"
65+
- match:
66+
path: "/cardano-wasm.js"
67+
response_headers_to_add:
68+
- header:
69+
key: "Content-Type"
70+
value: "text/javascript"
71+
direct_response:
72+
status: 200
73+
body:
74+
filename: "./cardano-wasm.js"
75+
- match:
76+
path: "/cardano-wasm.wasm"
77+
response_headers_to_add:
78+
- header:
79+
key: "Content-Type"
80+
value: "application/wasm"
81+
direct_response:
82+
status: 200
83+
body:
84+
filename: "./cardano-wasm.wasm"
85+
- match:
86+
path: "/example.js"
87+
response_headers_to_add:
88+
- header:
89+
key: "Content-Type"
90+
value: "text/javascript"
91+
direct_response:
92+
status: 200
93+
body:
94+
filename: "./example.js"
95+
- match:
96+
path: "/node_grpc_web_pb.js"
97+
response_headers_to_add:
98+
- header:
99+
key: "Content-Type"
100+
value: "text/javascript"
101+
direct_response:
102+
status: 200
103+
body:
104+
filename: "./node_grpc_web_pb.js"
105+
http_filters:
106+
- name: envoy.filters.http.grpc_web
107+
typed_config:
108+
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
109+
- name: envoy.filters.http.router
110+
typed_config:
111+
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
112+
113+
clusters:
114+
- name: grpc_service_unix_socket
115+
connect_timeout: 0.25s
116+
typed_extension_protocol_options:
117+
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
118+
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
119+
explicit_http_config:
120+
http2_protocol_options: {}
121+
load_assignment:
122+
cluster_name: grpc_service_unix_socket
123+
endpoints:
124+
- lb_endpoints:
125+
- endpoint:
126+
address:
127+
pipe:
128+
path: ../../rpc.socket
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import cardano_api from "./cardano-api.js";
2+
3+
let promise = cardano_api();
4+
5+
const output = document.createElement("code");
6+
output.innerText = "";
7+
output.id = "test-output";
8+
document.body.appendChild(output);
9+
10+
function log(out) {
11+
console.log(out);
12+
if (typeof(out) == "object") {
13+
output.innerText += "> [object] {\n";
14+
for (let [key, val] of Object.entries(out)) {
15+
let text = val.toString();
16+
if (typeof(val) == "function") {
17+
text = text.split("{")[0];
18+
}
19+
output.innerText += "    " + key + ": " + text + "\n";
20+
}
21+
output.innerText += "  }\n";
22+
} else {
23+
output.innerText += "> " + JSON.stringify(out) + "\n";
24+
}
25+
}
26+
27+
function finish_test() {
28+
let finishTag = document.createElement("p");
29+
finishTag.innerText = "Finished test!";
30+
finishTag.id = "finish-tag";
31+
document.body.appendChild(finishTag);
32+
}
33+
34+
async function do_async_work() {
35+
let api = await promise;
36+
log("Api object:");
37+
log(api);
38+
39+
let grpcApi = await api.newGrpcConnection("http://localhost:8080")
40+
41+
log("GRPC connection object:");
42+
log(grpcApi);
43+
44+
let eraNum = await grpcApi.getEra();
45+
log("Era number:");
46+
log(eraNum);
47+
48+
finish_test();
49+
}
50+
51+
do_async_work().then(() => { });
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<html>
2+
<head>
3+
<title>cardano-wasm test</title>
4+
</head>
5+
<body>
6+
<script type="module" src="./node_grpc_web_pb.js"></script>
7+
<script type="module" src="./example.js"></script>
8+
<h1>Test output</h1>
9+
</body>
10+
11+
</html>

cardano-wasm/js-test/basic-test.spec.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { test, expect } from '@playwright/test';
22

33
test('test output matches', async ({ page }) => {
4+
// Navigate to the test page
45
await page.goto('http://localhost:8080');
6+
// Wait for the page to load and the window title to be set (it should be "cardano-wasm test")
57
await expect(page).toHaveTitle(/cardano-wasm test/);
8+
// Wait for the test to finish running (we signal this by creating a tag with id "finish-tag" and text "Finished test!")
69
await expect(page.locator('#finish-tag')).toHaveText("Finished test!");
7-
await expect(page.locator('#test-output')).toHaveText('> "Api object:"> [object] { objectType: cardano-api newConwayTx: async function (...args) }> "UnsignedTx object:"> [object] { objectType: UnsignedTx addTxInput: function(txId,txIx) addSimpleTxOut: function(destAddr,lovelaceAmount) setFee: function(lovelaceAmount) estimateMinFee: function(protocolParams,numKeyWitnesses,numByronKeyWitnesses,totalRefScriptSize) signWithPaymentKey: function(signingKey) }> "Estimated fee:"> 164005> "SignedTx object:"> [object] { objectType: SignedTx alsoSignWithPaymentKey: function(signingKey) txToCbor: function() }> "Tx CBOR:"> "84a300d9010281825820be6efd42a3d7b9a00d09d77a5d41e55ceaf0bd093a8aa8a893ce70d9caafd97800018182581d6082935e44937e8b530f32ce672b5d600d0a286b4e8a52c6555f659b871a00989680021a000280a5a100d9010281825820adfc1c30385916da87db1ba3328f0690a57ebb2a6ac9f6f86b2d97f943adae005840a49259b5977aea523b46f01261fbff93e0899e8700319e11f5ab96b67eb628fca1a233ce2d50ee3227b591b84f27237d920d63974d65728362382f751c4d9400f5f6"');
10+
// Check the output of the test (from the example folder), which is displayed in the code element with id "test-output". The output contains information about the various objects and results of trying some of the functions.
11+
await expect(page.locator('#test-output')).toHaveText('> "Api object:"> [object] { objectType: cardano-api newConwayTx: async function (...args) newGrpcConnection: async function (...args) }> "UnsignedTx object:"> [object] { objectType: UnsignedTx addTxInput: function(txId,txIx) addSimpleTxOut: function(destAddr,lovelaceAmount) setFee: function(lovelaceAmount) estimateMinFee: function(protocolParams,numKeyWitnesses,numByronKeyWitnesses,totalRefScriptSize) signWithPaymentKey: function(signingKey) }> "Estimated fee:"> 164005> "SignedTx object:"> [object] { objectType: SignedTx alsoSignWithPaymentKey: function(signingKey) txToCbor: function() }> "Tx CBOR:"> "84a300d9010281825820be6efd42a3d7b9a00d09d77a5d41e55ceaf0bd093a8aa8a893ce70d9caafd97800018182581d6082935e44937e8b530f32ce672b5d600d0a286b4e8a52c6555f659b871a00989680021a000280a5a100d9010281825820adfc1c30385916da87db1ba3328f0690a57ebb2a6ac9f6f86b2d97f943adae005840a49259b5977aea523b46f01261fbff93e0899e8700319e11f5ab96b67eb628fca1a233ce2d50ee3227b591b84f27237d920d63974d65728362382f751c4d9400f5f6"');
812
});
9-

cardano-wasm/lib-wrapper/cardano-api.d.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,22 @@ declare interface SignedTx {
8181
txToCbor(): Promise<string>;
8282
}
8383

84+
/**
85+
* Represents a gRPC-web client connection to a Cardano node.
86+
*/
87+
declare interface GrpcConnection {
88+
/**
89+
* The type of the object, used for identification (the "GrpcConnection" string).
90+
*/
91+
objectType: string;
92+
93+
/**
94+
* Get the era from the Cardano Node using a GRPC-web client.
95+
* @returns A promise that resolves to the current era number.
96+
*/
97+
getEra(): Promise<number>;
98+
}
99+
84100
/**
85101
* The main Cardano API object with static methods.
86102
*/
@@ -95,5 +111,12 @@ declare interface CardanoAPI {
95111
* @returns A promise that resolves to a new `UnsignedTx` object.
96112
*/
97113
newConwayTx(): Promise<UnsignedTx>;
114+
115+
/**
116+
* Create a new client connection for communicating with a Cardano node through gRPC-web.
117+
* @param webGrpcUrl The URL of the gRPC-web server.
118+
* @returns A promise that resolves to a new `GrpcConnection`.
119+
*/
120+
newGrpcConnection(webGrpcUrl: string): Promise<GrpcConnection>;
98121
}
99122

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module Cardano.Wasm.Internal.Api.GRPC where
2+
3+
import Cardano.Wasm.Internal.JavaScript.GRPCTypes (JSGRPCClient)
4+
5+
-- | Internal data for the GrpcConnection virtual object. Currently, it is just a wrapper around the JSGRPCClient,
6+
-- which is a JavaScript object that allows us to interact with the Cardano Node via gRPC-web.
7+
newtype GrpcObject
8+
= GrpcObject JSGRPCClient
9+
10+
-- | Create a new unsigned transaction object for making a Conway era transaction.
11+
newGrpcConnectionImpl :: (String -> IO JSGRPCClient) -> String -> IO GrpcObject
12+
newGrpcConnectionImpl createClientJsFunc host = GrpcObject <$> createClientJsFunc host
13+
14+
-- | Get the era from the Cardano Node using GRPC-web.
15+
getEraImpl :: (JSGRPCClient -> IO Int) -> GrpcObject -> IO Int
16+
getEraImpl getEraJsFunc (GrpcObject client) = getEraJsFunc client

cardano-wasm/src/Cardano/Wasm/Internal/Api/Info.hs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ apiInfo :: ApiInfo
126126
apiInfo =
127127
let unsignedTxObjectName = "UnsignedTx"
128128
signedTxObjectName = "SignedTx"
129+
grpcConnectionName = "GrpcConnection"
129130

130131
unsignedTxObj =
131132
VirtualObjectInfo
@@ -206,6 +207,21 @@ apiInfo =
206207
}
207208
]
208209
}
210+
211+
grpcConnection =
212+
VirtualObjectInfo
213+
{ virtualObjectName = grpcConnectionName
214+
, virtualObjectDoc = "Represents a gRPC-web client connection to a Cardano node."
215+
, virtualObjectMethods =
216+
[ MethodInfo
217+
{ methodName = "getEra"
218+
, methodDoc = "Get the era from the Cardano Node using a GRPC-web client."
219+
, methodParams = []
220+
, methodReturnType = OtherType "number"
221+
, methodReturnDoc = "A promise that resolves to the current era number."
222+
}
223+
]
224+
}
209225
in ApiInfo
210226
{ mainObject =
211227
VirtualObjectInfo
@@ -219,9 +235,16 @@ apiInfo =
219235
, methodReturnType = NewObject unsignedTxObjectName
220236
, methodReturnDoc = "A promise that resolves to a new `UnsignedTx` object."
221237
}
238+
, MethodInfo
239+
{ methodName = "newGrpcConnection"
240+
, methodDoc = "Create a new client connection for communicating with a Cardano node through gRPC-web."
241+
, methodParams = [ParamInfo "webGrpcUrl" "string" "The URL of the gRPC-web server."]
242+
, methodReturnType = NewObject grpcConnectionName
243+
, methodReturnDoc = "A promise that resolves to a new `GrpcConnection`."
244+
}
222245
]
223246
}
224-
, virtualObjects = [unsignedTxObj, signedTxObj]
247+
, virtualObjects = [unsignedTxObj, signedTxObj, grpcConnection]
225248
, initialiseFunctionDoc = "Initialises the Cardano API."
226249
, initialiseFunctionReturnDoc = "A promise that resolves to the main `CardanoAPI` object."
227250
}

0 commit comments

Comments
 (0)