feat(eth): Use rust-web3 to serialize and serialize ETH calls (#417)

This commit is contained in:
folex 2023-02-06 19:05:50 +07:00 committed by GitHub
parent b537cdf345
commit 8999fcec85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 940 additions and 0 deletions

View File

@ -0,0 +1,24 @@
[package]
name = "eth-rpc"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "eth-rpc"
path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
web3 = { version = "0.18.0", features = [], default-features = false }
#async-std = "1.12.0" # async-std does not support wasm32-wasi
serde_json = "1.0.91"
serde = "1.0.152"
jsonrpc-core = "18.0.0"
tokio = { version = "1.24.1", default-features = false, features = ["rt"] }
eyre = "0.6.8"
marine-rs-sdk = "0.7.1"
[dev-dependencies]
marine-rs-sdk-test = "0.8.1"

View File

@ -0,0 +1,16 @@
# Build eth-rpc.wasm
```shell
marine build --release
```
# Build curl-adapter.wasm
```shell
cd ../curl-adapter
marine build --release
```
# Run tests
```shell
./test.sh
```
## It works!~

View File

@ -0,0 +1,87 @@
data RPCResponse:
value: string
success: bool
error: ?string
data RPCResult:
stdout: RPCResponse -- JSON-RPC result
stderr: string -- curl error and other non json-rpc errors
data Web3Balance:
balance: ?string -- hex string bignum
success: bool
error: ?string
data Web3GasUsed:
gas_used: ?string -- hex string
success: bool
error: ?string
data Web3EthCall:
result: ?string -- hex string
success: bool
error: ?string
service ParseToWeb3Balances("json"):
parse(s:string) -> Web3Balance
-- e.g., https://docs.infura.io/infura/networks/ethereum/json-rpc-methods/eth_getbalance
service Web3Services("service-id"):
call_eth_get_balance() -> RPCResult
call_eth_estimate_gas() -> RPCResult
call_eth_call() -> RPCResult
-- rpc_params: account id, blockheight: ususally "latest"
-- or we create a data struct and serialize it in aqua to []string
func eth_ getBalance(peerid: string, service_id: string, uri: string, rpc_params: Vec<String>, nonce: u32) -> Web3Balance:
result: *Web3Balance
on peerid:
Web3Services service_id
res <- Web3Services.call_eth_get_balance(uri, rpc_params, nonce)
if res.stdout.success==true:
result.balance <- res.stdout.value
result.success <<- true
else:
result.success <<- false
result.error <<- res.stdout.value
<- result[0]
-- here the data struct approach seems to make even more sense as we need the transaction call object:
-- data TObject:
-- from: ?[]u8 -- optional 20 bytes, address tx is sent from
-- to: []u8 -- 20 bytes to address
-- gas: ?string -- gas provided for execution of method haxadecimal
-- gasPrice: ?string -- gasPrice used, hexadecimal
-- maxFeesPerGase: ?string -- maximum fee in wei
-- value: ?string -- value sent with tx, hexadecimal
-- data: ?string -- hash of method signature and encoded params
--func eth_estimateGas(peerid: string, service_id: string, uri: string, t_obj: TObject, nonce: u32) -> Web3GasUsed:
-- the "easy" way: Vec<String>
func eth_estimateGas(peerid: string, service_id: string, uri: string, rpc_params: []string, nonce: u32) -> Web3GasUsed:
result: *Web3Gas
on peerid:
Web3Services service_id
res <- Web3Services.call_eth_estimate_gas(uri, rpc_params, nonce)
if res.stdout.success==true:
result.gas_used <- res.stdout.value
result.success <<- true
else:
result.success <<- false
result.error <<- res.stdout.value
<- result
-- also a big intake object, e.g. https://docs.infura.io/infura/networks/ethereum/json-rpc-methods/eth_call
-- easy way -- client serializes to []string
func eth_call(peerid: string, service_id: string, uri: string, rpc_params: []string, nonce: u32) -> Web3EthCall:
result: *Web3EthCall
on peerid:
Web3Services service_id
res <- Web3Services.call_eth_call(uri, rpc_params, nonce)
if res.stdout.success==true:
result.result <- res.stdout.value
result.success <<- true
else:
result.success <<- false
result.error <<- res.stdout.error ---not sure if this is how Web3 does it if there is a Revert error, e.g. https://docs.infura.io/infura/networks/ethereum/json-rpc-methods/eth_call
<- result

View File

@ -0,0 +1,66 @@
-- https://www.jsonrpc.org/specification
data RPCError:
code: i32
message: string
data: ?string
data RPCResponse:
jsonrpc: string
result: string
error: ?RPCError
id: u32
data RPCResponse2:
value: string
success: bool
error: ?string
data RPCResult:
stdout: RPCResponse -- JSON-RPC result
stderr: string -- curl error and other non json-rpc errors
data RPCResult2:
stdout: RPCResponse2 -- JSON-RPC result
stderr: string -- curl error and other non json-rpc errors
data Web3Accounts:
accounts: []string
service ParseToAccounts("json"):
parse(s:string) -> Web3Accounts
service Web3Services("service-id"):
call_get_accounts() -> [][]u8
call_get_accounts_json() -> RPCResult
call_get_accounts_json_2() -> RPCResult2
-- the bytestring return which allows you to do nothing until you convert
-- the bytes using another service to be deployed and a pita to sort through
-- error types
func get_accounts(peerid: string, service_id: string) -> [][]u8:
on peerid:
Web3Services service_id
res <- Web3Services.call_get_accounts()
<- res
func get_accounts_jstring(peerid: string, service_id: string) -> Web3Accounts:
on peerid:
Web3Services service_id
res <- Web3Services.call_get_accounts_json()
-- if not error ...
-- if not rpc error
accounts <- ParseToAccounts.parse(res.stdout.result)
<- accounts
func get_accounts_jstring_2(peerid: string, service_id: string) -> Web3Accounts:
on peerid:
Web3Services service_id
res <- Web3Services.call_get_accounts_json_2()
-- if not error ....
if res.stdout.success == true:
accounts <- ParseToAccounts.parse(res.stdout.value)
<- accounts

View File

@ -0,0 +1,4 @@
[toolchain]
channel = "nightly-2022-12-06"
targets = [ "x86_64-unknown-linux-gnu" ]
components = [ "rustfmt" ]

View File

@ -0,0 +1,120 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use jsonrpc_core::types::request::Call;
use jsonrpc_core::Output;
use serde_json::json;
use serde_json::Value;
use web3::futures::future::BoxFuture;
use web3::{RequestId, Transport};
use crate::curl_request;
pub type FutResult = BoxFuture<'static, web3::error::Result<Value>>;
#[derive(Debug, Clone)]
pub struct CurlTransport {
pub uri: String,
id: Arc<AtomicUsize>,
}
impl CurlTransport {
pub fn new(uri: String) -> Self {
Self {
uri,
id: Arc::new(AtomicUsize::new(0)),
}
}
pub fn next_id(&self) -> RequestId {
self.id.fetch_add(1, Ordering::AcqRel)
}
}
impl Transport for CurlTransport {
type Out = FutResult;
fn prepare(&self, method: &str, params: Vec<Value>) -> (RequestId, Call) {
let id = self.next_id();
let request = web3::helpers::build_request(id, method, params.clone());
(id, request)
}
fn send(&self, _: RequestId, call: Call) -> Self::Out {
if let Call::MethodCall(call) = call {
/*
curl --request POST \
--url $uri \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '
{
"id": 1,
"jsonrpc": "2.0",
"method": "eth_accounts"
}
'
*/
let uri = self.uri.clone();
Box::pin(async move {
let json = json!(call).to_string();
let args = vec![
"--request",
"POST",
"--url",
&uri,
"--header",
"accept: application/json",
"--header",
"content-type: application/json",
"--data",
json.as_str(),
];
let args = args.into_iter().map(|s| s.to_string()).collect();
let response = curl_request(args);
/*
println!(
"response is: \nstdout: {:?}\nstderr: {:?}",
String::from_utf8(response.stdout.clone()),
String::from_utf8(response.stderr.clone())
);
println!("slice: {:?}", serde_json::from_value::<Output>(serde_json::from_slice(response.stdout.as_slice())?));
*/
// FIX: if there's a bad uri, the panic kicks in here.
let response: Output =
serde_json::from_value(serde_json::from_slice(response.stdout.as_slice())?)?;
let result = match response {
Output::Success(jsonrpc_core::types::Success { result, .. }) => result,
// no sure if that's enough vs the complete jsonrpc error msg
Output::Failure(jsonrpc_core::types::Failure { error, .. }) => {
serde_json::to_value(error.message).unwrap()
} /*
Output::Failure(failure) => panic!(
"JSON RPC response was a failure {}",
json!(failure).to_string()
),
*/
/*
Output::Failure(failure) => {
let err = jsonrpc_core::types::error::Error.parse_error()
}
format!("JSON RPC response was a failure {}",
json!(failure).to_string()),
*/
};
// println!("parsed result is {}", result.to_string());
Ok(result)
})
} else {
todo!()
}
// Box::pin(async { Ok(json!(["0x407d73d8a49eeb85d32cf465507dd71d507100c1"])) })
}
}

View File

@ -0,0 +1,103 @@
use marine_rs_sdk::marine;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::runtime::Builder;
use web3::Transport;
use crate::curl_transport::CurlTransport;
use crate::values::JsonString;
#[marine]
pub fn eth_call(uri: String, method: &str, json_args: Vec<String>) -> JsonString {
let result: eyre::Result<Value> = try {
let rt = Builder::new_current_thread().build()?;
let args: Result<Vec<Value>, _> = json_args
.into_iter()
.map(|a| serde_json::from_str(&a))
.collect();
let transport = CurlTransport::new(uri);
let result = rt.block_on(transport.execute(method, args?))?;
result
};
result.into()
}
#[marine]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RPCResult {
provider_name: String,
stdout: String,
stderr: String,
}
pub fn eth_call_2(uri: String, method: &str, json_args: Vec<String>) -> JsonString {
let result: eyre::Result<Value> = try {
let rt = Builder::new_current_thread().build()?;
let args: Result<Vec<Value>, _> = json_args
.into_iter()
.map(|a| serde_json::from_str(&a))
.collect();
let transport = CurlTransport::new(uri);
let result = rt.block_on(transport.execute(method, args?))?;
result
};
result.into()
}
#[cfg(test)]
mod tests {
use marine_rs_sdk_test::marine_test;
#[marine_test(
config_path = "../tests_artifacts/Config.toml",
modules_dir = "../tests_artifacts"
)]
fn get_accounts_bad_uri(rpc: marine_test_env::eth_rpc::ModuleInterface) {
let uri: String = "http://bad_uri.to".into();
let method: String = "eth_accounts".into();
let json_args: Vec<String> = vec![];
let accounts = rpc.eth_call(uri, method, json_args);
println!("bad uri call: {:?}", accounts);
// println!("accounts: {:?}", accounts);
// assert_eq!(accounts.len(), 0);
}
#[marine_test(
config_path = "../tests_artifacts/Config.toml",
modules_dir = "../tests_artifacts"
)]
fn get_accounts_bad_method(rpc: marine_test_env::eth_rpc::ModuleInterface) {
let uri: String = std::fs::read_to_string("./infura_uri.txt").unwrap();
let method: String = "eth_getAccounts".into();
let json_args: Vec<String> = vec![];
let accounts = rpc.eth_call(uri, method, json_args);
println!("bad method: {:?}", accounts);
// println!("accounts: {:?}", accounts);
// assert_eq!(accounts.len(), 0);
}
#[marine_test(
config_path = "../tests_artifacts/Config.toml",
modules_dir = "../tests_artifacts"
)]
fn get_accounts_good_uri(rpc: marine_test_env::eth_rpc::ModuleInterface) {
let uri: String = std::fs::read_to_string("./infura_uri.txt").unwrap();
let method: String = "eth_accounts".into();
let json_args: Vec<String> = vec![];
let accounts = rpc.eth_call(uri, method, json_args);
println!("all good: {:?}", accounts);
// println!("accounts: {:?}", accounts);
// assert_eq!(accounts.len(), 0);
}
}

View File

@ -0,0 +1,104 @@
#![feature(try_blocks)]
use marine_rs_sdk::module_manifest;
use marine_rs_sdk::{marine, MountedBinaryResult};
use tokio::runtime::Builder;
use web3::api::Eth;
use web3::helpers::CallFuture;
use web3::types::Address;
use web3::Web3;
use crate::curl_transport::{CurlTransport, FutResult};
pub mod curl_transport;
pub mod eth_call;
pub mod typed;
pub mod values;
module_manifest!();
pub fn main() {}
// #[tokio::main(flavor = "current_thread")]
// flavor idea comes from https://github.com/rjzak/tokio-echo-test/blob/main/src/main.rs#L42
// but seems to require additional tokio futures
pub fn get_accounts(uri: String) -> web3::error::Result<Vec<Vec<u8>>> {
let rt = Builder::new_current_thread().build()?;
let web3 = Web3::new(CurlTransport::new(uri));
let eth = web3.eth();
println!("Calling accounts.");
let accounts: CallFuture<Vec<Address>, FutResult> = eth.accounts();
let accounts: web3::Result<Vec<Address>> = rt.block_on(accounts);
println!("Accounts: {:?}", accounts);
Ok(accounts?
.into_iter()
.map(|a: Address| a.as_bytes().to_vec())
.collect())
}
pub fn web3_call<
Out: serde::de::DeserializeOwned,
F: FnOnce(Eth<CurlTransport>) -> CallFuture<Out, FutResult>,
>(
uri: String,
call: F,
) -> web3::error::Result<Out> {
let rt = Builder::new_current_thread()
.build()
.expect("error starting tokio runtime");
let web3 = Web3::new(CurlTransport::new(uri));
let result: CallFuture<Out, FutResult> = call(web3.eth());
let result: web3::error::Result<Out> = rt.block_on(result);
result
}
#[marine]
pub fn call_get_accounts(uri: String) -> Vec<Vec<u8>> {
get_accounts(uri).expect("error calling main")
}
#[marine]
#[link(wasm_import_module = "curl_adapter")]
extern "C" {
pub fn curl_request(cmd: Vec<String>) -> MountedBinaryResult;
}
#[cfg(test)]
mod tests {
use marine_rs_sdk_test::marine_test;
// use web3::types::Address;
#[marine_test(
config_path = "../tests_artifacts/Config.toml",
modules_dir = "../tests_artifacts"
)]
fn get_accounts(rpc: marine_test_env::eth_rpc::ModuleInterface) {
let uri: String = std::fs::read_to_string("./infura_uri.txt").unwrap();
let accounts = rpc.call_get_accounts(uri);
// let addr: Address = "0x407d73d8a49eeb85d32cf465507dd71d507100c1"
// .parse()
// .unwrap();
// assert_eq!(accounts, vec![addr.as_bytes().to_vec()]);
assert_eq!(accounts.len(), 0);
}
#[marine_test(
config_path = "../tests_artifacts/Config.toml",
modules_dir = "../tests_artifacts"
)]
fn get_accounts_generic(rpc: marine_test_env::eth_rpc::ModuleInterface) {
let uri: String = std::fs::read_to_string("./infura_uri.txt").unwrap();
let method: String = "eth_accounts".into();
let json_args: Vec<String> = vec![];
let accounts = rpc.eth_call(uri, method, json_args);
println!("accounts: {:?}", accounts);
// assert_eq!(accounts.len(), 0);
}
}

View File

@ -0,0 +1,263 @@
#![allow(unused)]
use marine_rs_sdk::marine;
use web3::types::{BlockId, BlockNumber, Bytes, CallRequest};
use crate::values::{BytesValue, JsonString, U64Value};
use crate::web3_call;
/// Get list of available accounts.
#[marine]
pub fn accounts(uri: String) -> Vec<JsonString> {
web3_call(uri, |w| w.accounts())
.into_iter()
.map(|a| {
let json = serde_json::to_value(&a).map_err(eyre::Report::from);
JsonString::from(json)
})
.collect()
}
/// Get current block number
#[marine]
pub fn block_number(uri: String) -> U64Value {
web3_call(uri, |w| w.block_number()).into()
}
/// Call a constant method of contract without changing the state of the blockchain.
#[marine]
pub fn call(uri: String, req: String, block: u64) -> BytesValue {
let result: eyre::Result<Bytes> = try {
let req: CallRequest = serde_json::from_str(&req)?;
web3_call(uri, move |w| w.call(req, Some(BlockId::Number(BlockNumber::Number(block.into())))))?
};
result.into()
}
/// Get coinbase address
// #[marine]
pub fn coinbase(uri: String) -> String {
todo!()
}
/// Compile LLL
// #[marine]
pub fn compile_lll(uri: String, code: String) -> Vec<u8> {
todo!()
}
/// Compile Solidity
// #[marine]
pub fn compile_solidity(uri: String, code: String) -> Vec<u8> {
todo!()
}
/// Compile Serpent
// #[marine]
pub fn compile_serpent(uri: String, code: String) -> Vec<u8> {
todo!()
}
/// Call a contract without changing the state of the blockchain to estimate gas usage.
// #[marine]
pub fn estimate_gas(uri: String, req: String, block: String) -> String {
todo!()
}
/// Get current recommended gas price
// #[marine]
pub fn gas_price(uri: String) -> String {
todo!()
}
/// Returns a collection of historical gas information. This can be used for evaluating the max_fee_per_gas
/// and max_priority_fee_per_gas to send the future transactions.
// #[marine]
pub fn fee_history(
uri: String,
block_count: String,
newest_block: String,
reward_percentiles: Vec<f64>,
) -> String {
todo!()
}
/// Get balance of given address
// #[marine]
pub fn balance(uri: String, address: String, block: String) -> String {
todo!()
}
/// Get all logs matching a given filter object
// #[marine]
pub fn logs(uri: String, filter: String) -> Vec<String> {
todo!()
}
/// Get block details with transaction hashes.
// #[marine]
pub fn block(uri: String, block: String) -> String {
todo!()
}
/// Get block details with full transaction objects.
// #[marine]
pub fn block_with_txs(uri: String, block: String) -> String {
todo!()
}
/// Get number of transactions in block
// #[marine]
pub fn block_transaction_count(uri: String, block: String) -> String {
todo!()
}
/// Get code under given address
// #[marine]
pub fn code(uri: String, address: String, block: String) -> Vec<u8> {
todo!()
}
/// Get supported compilers
// #[marine]
pub fn compilers(uri: String) -> Vec<String> {
todo!()
}
/// Get chain id
// #[marine]
pub fn chain_id(uri: String) -> String {
todo!()
}
/// Get available user accounts. This method is only available in the browser. With MetaMask,
/// this will cause the popup that prompts the user to allow or deny access to their accounts
/// to your app.
// #[marine]
pub fn request_accounts(uri: String) -> Vec<String> {
todo!()
}
/// Get storage entry
// #[marine]
pub fn storage(uri: String, address: String, idx: String, block: String) -> String {
todo!()
}
/// Get nonce
// #[marine]
pub fn transaction_count(uri: String, address: String, block: String) -> String {
todo!()
}
/// Get transaction
// #[marine]
pub fn transaction(uri: String, id: String) -> String {
todo!()
}
/// Get transaction receipt
// #[marine]
pub fn transaction_receipt(uri: String, hash: String) -> String {
todo!()
}
/// Get uncle header by block ID and uncle index.
///
/// This method is meant for TurboGeth compatiblity,
/// which is missing transaction hashes in the response.
// #[marine]
pub fn uncle_header(uri: String, block: String, index: String) -> String {
todo!()
}
/// Get uncle by block ID and uncle index -- transactions only has hashes.
// #[marine]
pub fn uncle(uri: String, block: String, index: String) -> String {
todo!()
}
/// Get uncle count in block
// #[marine]
pub fn uncle_count(uri: String, block: String) -> String {
todo!()
}
/// Get work package
// #[marine]
pub fn work(uri: String) -> String {
todo!()
}
/// Get hash rate
// #[marine]
pub fn hashrate(uri: String) -> String {
todo!()
}
/// Get mining status
// #[marine]
pub fn mining(uri: String) -> bool {
todo!()
}
/// Start new block filter
// #[marine]
pub fn new_block_filter(uri: String) -> String {
todo!()
}
/// Start new pending transaction filter
// #[marine]
pub fn new_pending_transaction_filter(uri: String) -> String {
todo!()
}
/// Start new pending transaction filter
// #[marine]
pub fn protocol_version(uri: String) -> String {
todo!()
}
/// Sends a rlp-encoded signed transaction
// #[marine]
pub fn send_raw_transaction(uri: String, rlp: String) -> String {
todo!()
}
/// Sends a transaction transaction
// #[marine]
pub fn send_transaction(uri: String, tx: String) -> String {
todo!()
}
/// Signs a hash of given data
// #[marine]
pub fn sign(uri: String, address: String, data: String) -> String {
todo!()
}
/// Submit hashrate of external miner
// #[marine]
pub fn submit_hashrate(uri: String, rate: String, id: String) -> bool {
todo!()
}
/// Submit work of external miner
// #[marine]
pub fn submit_work(uri: String, nonce: String, pow_hash: String, mix_hash: String) -> bool {
todo!()
}
/// Get syncing status
// #[marine]
pub fn syncing(uri: String) -> String {
todo!()
}
/// Returns the account- and storage-values of the specified account including the Merkle-proof.
// #[marine]
pub fn proof(uri: String, address: String, keys: String, block: String) -> String {
todo!()
}

View File

@ -0,0 +1,76 @@
use marine_rs_sdk::marine;
use serde_json::Value;
use web3::types::Bytes;
use web3::types::U64;
#[marine]
pub struct JsonString {
value: String,
success: bool,
error: String,
}
impl From<eyre::Result<Value>> for JsonString {
fn from(value: eyre::Result<Value>) -> Self {
match value {
Ok(value) => JsonString {
value: value.to_string(),
success: true,
error: String::new(),
},
Err(err) => JsonString {
value: String::new(),
success: false,
error: format!("{}\n{:?}", err, err),
},
}
}
}
#[marine]
pub struct U64Value {
value: u64,
success: bool,
error: String,
}
impl From<web3::error::Result<U64>> for U64Value {
fn from(value: web3::error::Result<U64>) -> Self {
match value {
Ok(value) => U64Value {
value: value.as_u64(),
success: true,
error: String::new(),
},
Err(err) => U64Value {
value: u64::default(),
success: false,
error: format!("{}\n{:?}", err, err),
},
}
}
}
#[marine]
pub struct BytesValue {
value: Vec<u8>,
success: bool,
error: String,
}
impl From<eyre::Result<Bytes>> for BytesValue {
fn from(value: eyre::Result<Bytes>) -> Self {
match value {
Ok(value) => BytesValue {
value: value.0,
success: true,
error: String::new(),
},
Err(err) => BytesValue {
value: vec![],
success: false,
error: format!("{}\n{:?}", err, err),
},
}
}
}

View File

@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -o nounset
set -o errexit
set -o pipefail
cd "$(dirname "$0")"
mkdir -p tests_artifacts
# build eth-rpc.wasm
marine build --release
cp ./target/wasm32-wasi/release/eth-rpc.wasm tests_artifacts/
# build curl-adapter.wasm
(cd ../curl-adapter; marine build --release)
cp ../curl-adapter/target/wasm32-wasi/release/curl_adapter.wasm tests_artifacts/
#if [[ ! -f "tests_artifacts/sqlite3.wasm" ]]; then
# # download SQLite 3
# curl -L https://github.com/fluencelabs/sqlite/releases/download/v0.15.0_w/sqlite3.wasm -o tests_artifacts/sqlite3.wasm
#fi
# run tests
cargo nextest run --release --no-fail-fast --nocapture

View File

@ -0,0 +1,23 @@
modules_dir = "."
#[[module]]
#name = "sqlite3"
#mem_pages_count = 100
#logger_enabled = false
#[module.wasi]
#preopened_files = ["/tmp"]
#mapped_dirs = { "tmp" = "." }
[[module]]
name = "curl_adapter"
max_heap_size = "2 MiB"
logger_enabled = true
[module.mounted_binaries]
curl = "/usr/bin/curl"
[[module]]
name = "eth-rpc"
logger_enabled = true

View File

@ -0,0 +1,29 @@
# Fluence decentralized RPC Workshop
ETHDenver 2/27/2023
##Outline
## Intended bounty user experience
* embedd a the already created Fluence client peer into your dApp
## Deployment and code
* deploy eth-rpc service to each KRAS (?) peer at least once
* create Registry instance for deployed services
* create Aqua scaffold to interct with registry and modules
* create Fluence JS reference client to embed into dApp
## Introduction
Fluence decentralized RPC (fRPC) is a ready to use ???