From 8d3871d791c69af50038e2562051e41b4a44108d Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Sat, 15 Jan 2022 13:56:49 +0100 Subject: [PATCH] cln-grpc: Add result conversion generator to `msggen` This takes the Rust bindings and converts them into the generated protobuf bindings: > JSON-RPC -> Rust bindings -> grpc bindings -> protobuf --- cln-grpc/Makefile | 4 +- cln-grpc/src/convert.rs | 170 ++++++++++++++++++++++++++++++ contrib/msggen/msggen/__main__.py | 6 +- contrib/msggen/msggen/grpc.py | 98 +++++++++++++++++ 4 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 cln-grpc/src/convert.rs diff --git a/cln-grpc/Makefile b/cln-grpc/Makefile index 9eb6bc26e..f7a42b85f 100644 --- a/cln-grpc/Makefile +++ b/cln-grpc/Makefile @@ -2,7 +2,9 @@ cln-grpc-wrongdir: $(MAKE) -C .. cln-grpc-all CLN_GRPC_EXAMPLES := -CLN_GRPC_GENALL = cln-grpc/proto/node.proto +CLN_GRPC_GENALL = cln-grpc/proto/node.proto \ + cln-grpc/src/convert.rs + DEFAULT_TARGETS += $(CLN_GRPC_EXAMPLES) $(CLN_GRPC_GENALL) $(CLN_GRPC_GENALL): $(JSON_SCHEMA) diff --git a/cln-grpc/src/convert.rs b/cln-grpc/src/convert.rs new file mode 100644 index 000000000..c5c41f81b --- /dev/null +++ b/cln-grpc/src/convert.rs @@ -0,0 +1,170 @@ + +// This file was automatically derived from the JSON-RPC schemas in +// `doc/schemas`. Do not edit this file manually as it would get +// overwritten. + +use std::convert::From; +#[allow(unused_imports)] +use cln_rpc::model::{responses,requests}; +use crate::pb; + +#[allow(unused_variables)] +impl From<&responses::GetinfoAddress> for pb::GetinfoAddress { + fn from(c: &responses::GetinfoAddress) -> Self { + Self { + item_type: c.item_type as i32, + port: c.port.into(), + address: c.address.clone(), + } + } +} + +#[allow(unused_variables)] +impl From<&responses::GetinfoBinding> for pb::GetinfoBinding { + fn from(c: &responses::GetinfoBinding) -> Self { + Self { + item_type: c.item_type as i32, + address: c.address.clone(), + port: c.port.map(|v| v.into()), + socket: c.socket.clone(), + } + } +} + +#[allow(unused_variables)] +impl From<&responses::GetinfoResponse> for pb::GetinfoResponse { + fn from(c: &responses::GetinfoResponse) -> Self { + Self { + id: hex::decode(&c.id).unwrap(), + alias: c.alias.clone(), + color: hex::decode(&c.color).unwrap(), + num_peers: c.num_peers.clone(), + num_pending_channels: c.num_pending_channels.clone(), + num_active_channels: c.num_active_channels.clone(), + num_inactive_channels: c.num_inactive_channels.clone(), + version: c.version.clone(), + lightning_dir: c.lightning_dir.clone(), + blockheight: c.blockheight.clone(), + network: c.network.clone(), + fees_collected_msat: Some(c.fees_collected_msat.into()), + address: c.address.iter().map(|s| s.into()).collect(), + binding: c.binding.iter().map(|s| s.into()).collect(), + warning_bitcoind_sync: c.warning_bitcoind_sync.clone(), + warning_lightningd_sync: c.warning_lightningd_sync.clone(), + } + } +} + +#[allow(unused_variables)] +impl From<&responses::ListfundsOutputs> for pb::ListfundsOutputs { + fn from(c: &responses::ListfundsOutputs) -> Self { + Self { + txid: hex::decode(&c.txid).unwrap(), + output: c.output.clone(), + amount_msat: Some(c.amount_msat.into()), + scriptpubkey: hex::decode(&c.scriptpubkey).unwrap(), + address: c.address.clone(), + redeemscript: c.redeemscript.as_ref().map(|v| hex::decode(&v).unwrap()), + status: c.status as i32, + blockheight: c.blockheight.clone(), + } + } +} + +#[allow(unused_variables)] +impl From<&responses::ListfundsChannels> for pb::ListfundsChannels { + fn from(c: &responses::ListfundsChannels) -> Self { + Self { + peer_id: hex::decode(&c.peer_id).unwrap(), + our_amount_msat: Some(c.our_amount_msat.into()), + amount_msat: Some(c.amount_msat.into()), + funding_txid: hex::decode(&c.funding_txid).unwrap(), + funding_output: c.funding_output.clone(), + connected: c.connected.clone(), + state: c.state as i32, + short_channel_id: c.short_channel_id.clone(), + } + } +} + +#[allow(unused_variables)] +impl From<&responses::ListfundsResponse> for pb::ListfundsResponse { + fn from(c: &responses::ListfundsResponse) -> Self { + Self { + outputs: c.outputs.iter().map(|s| s.into()).collect(), + channels: c.channels.iter().map(|s| s.into()).collect(), + } + } +} + +#[allow(unused_variables)] +impl From<&responses::ListchannelsChannels> for pb::ListchannelsChannels { + fn from(c: &responses::ListchannelsChannels) -> Self { + Self { + source: hex::decode(&c.source).unwrap(), + destination: hex::decode(&c.destination).unwrap(), + public: c.public.clone(), + amount_msat: Some(c.amount_msat.into()), + message_flags: c.message_flags.into(), + channel_flags: c.channel_flags.into(), + active: c.active.clone(), + last_update: c.last_update.clone(), + base_fee_millisatoshi: c.base_fee_millisatoshi.clone(), + fee_per_millionth: c.fee_per_millionth.clone(), + delay: c.delay.clone(), + htlc_minimum_msat: Some(c.htlc_minimum_msat.into()), + htlc_maximum_msat: c.htlc_maximum_msat.map(|f| f.into()), + features: hex::decode(&c.features).unwrap(), + } + } +} + +#[allow(unused_variables)] +impl From<&responses::ListchannelsResponse> for pb::ListchannelsResponse { + fn from(c: &responses::ListchannelsResponse) -> Self { + Self { + channels: c.channels.iter().map(|s| s.into()).collect(), + } + } +} + +#[allow(unused_variables)] +impl From<&responses::AddgossipResponse> for pb::AddgossipResponse { + fn from(c: &responses::AddgossipResponse) -> Self { + Self { + } + } +} + +#[allow(unused_variables)] +impl From<&responses::AutocleaninvoiceResponse> for pb::AutocleaninvoiceResponse { + fn from(c: &responses::AutocleaninvoiceResponse) -> Self { + Self { + enabled: c.enabled.clone(), + expired_by: c.expired_by.clone(), + cycle_seconds: c.cycle_seconds.clone(), + } + } +} + +#[allow(unused_variables)] +impl From<&responses::CheckmessageResponse> for pb::CheckmessageResponse { + fn from(c: &responses::CheckmessageResponse) -> Self { + Self { + verified: c.verified.clone(), + pubkey: c.pubkey.as_ref().map(|v| hex::decode(&v).unwrap()), + } + } +} + +#[allow(unused_variables)] +impl From<&responses::CloseResponse> for pb::CloseResponse { + fn from(c: &responses::CloseResponse) -> Self { + Self { + item_type: c.item_type as i32, + tx: c.tx.as_ref().map(|v| hex::decode(&v).unwrap()), + txid: c.txid.as_ref().map(|v| hex::decode(&v).unwrap()), + } + } +} + diff --git a/contrib/msggen/msggen/__main__.py b/contrib/msggen/msggen/__main__.py index 219ddf4a5..01585c83a 100644 --- a/contrib/msggen/msggen/__main__.py +++ b/contrib/msggen/msggen/__main__.py @@ -1,5 +1,5 @@ from msggen.model import Method, CompositeField, Service -from msggen.grpc import GrpcGenerator +from msggen.grpc import GrpcGenerator, GrpcConverterGenerator from msggen.rust import RustGenerator from pathlib import Path import subprocess @@ -132,6 +132,10 @@ def gengrpc(service): fname = repo_root() / "cln-grpc" / "proto" / "node.proto" dest = open(fname, "w") GrpcGenerator(dest).generate(service) + + fname = repo_root() / "cln-grpc" / "src" / "convert.rs" + dest = open(fname, "w") + GrpcConverterGenerator(dest).generate(service) def genrustjsonrpc(service): fname = repo_root() / "cln-rpc" / "src" / "model.rs" dest = open(fname, "w") diff --git a/contrib/msggen/msggen/grpc.py b/contrib/msggen/msggen/grpc.py index d90b14f4d..54d4976c8 100644 --- a/contrib/msggen/msggen/grpc.py +++ b/contrib/msggen/msggen/grpc.py @@ -151,3 +151,101 @@ class GrpcGenerator: for message in [f for f in fields if isinstance(f, CompositeField)]: self.generate_message(message) + + +class GrpcConverterGenerator: + def __init__(self, dest: TextIO): + self.dest = dest + self.logger = logging.getLogger("msggen.grpc.GrpcConversionGenerator") + + def generate_array(self, prefix, field: ArrayField): + if isinstance(field.itemtype, CompositeField): + self.generate_composite(prefix, field.itemtype) + + def generate_composite(self, prefix, field: CompositeField): + """Generates the conversions from JSON-RPC to GRPC. + """ + # First pass: generate any sub-fields before we generate the + # top-level field itself. + for f in field.fields: + if isinstance(f, ArrayField): + self.generate_array(prefix, f) + + # And now we can convert the current field: + self.write(f"""\ + #[allow(unused_variables)] + impl From<&{prefix}::{field.typename}> for pb::{field.typename} {{ + fn from(c: &{prefix}::{field.typename}) -> Self {{ + Self {{ + """) + + for f in field.fields: + name = f.normalized() + if isinstance(f, ArrayField): + self.write(f"{name}: c.{name}.iter().map(|s| s.into()).collect(),\n", numindent=3) + + elif isinstance(f, EnumField): + self.write(f"{name}: c.{name} as i32,\n", numindent=3) + + elif isinstance(f, PrimitiveField): + typ = f.typename + ("?" if not f.required else "") + # We may need to reduce or increase the size of some + # types, or have some conversion such as + # hex-decoding. Also includes the `Some()` that grpc + # requires for non-native types. + rhs = { + 'u8': f'c.{name}.into()', + 'u16': f'c.{name}.into()', + 'u16?': f'c.{name}.map(|v| v.into())', + 'msat': f'Some(c.{name}.into())', + 'msat?': f'c.{name}.map(|f| f.into())', + 'pubkey': f'hex::decode(&c.{name}).unwrap()', + 'pubkey?': f'c.{name}.as_ref().map(|v| hex::decode(&v).unwrap())', + 'hex': f'hex::decode(&c.{name}).unwrap()', + 'hex?': f'c.{name}.as_ref().map(|v| hex::decode(&v).unwrap())', + 'txid': f'hex::decode(&c.{name}).unwrap()', + 'txid?': f'c.{name}.as_ref().map(|v| hex::decode(&v).unwrap())', + }.get( + typ, + f'c.{name}.clone()' # default to just assignment + ) + self.write(f"{name}: {rhs},\n", numindent=3) + + self.write(f"""\ + }} + }} + }} + + """) + + def generate_requests(self, service): + for meth in service.methods: + req = meth.request + self.generate_composite("requests", req) + + def generate_responses(self, service): + for meth in service.methods: + res = meth.response + self.generate_composite("responses", res) + + def generate(self, service: Service) -> None: + self.write(""" + // This file was automatically derived from the JSON-RPC schemas in + // `doc/schemas`. Do not edit this file manually as it would get + // overwritten. + + use std::convert::From; + #[allow(unused_imports)] + use cln_rpc::model::{responses,requests}; + use crate::pb; + + """) + + self.generate_responses(service) + + def write(self, text: str, numindent: int = 0) -> None: + raw = dedent(text) + if numindent > 0: + raw = indent(text, " " * numindent) + + self.dest.write(raw)