#How to Ripple?
Contracts are the building blocks for the Ripple 2.0 ecosystem. Here is an ADR explaining Contracts in detail
Contracts are available in
core/sdk/src/framework/ripple_contract.rs
Before adding a new contract check if a similar one already exists.
To add a new contract follow the camelCase
naming convention.
Add enough information on top of the entry using Rust docs used in the earlier contracts.
Most contracts are mapped to a Request structure through the implementation of the ExtnPayloadProvider
trait.
Below is an example for the same
pub enum CustomRequest {
Get,
Set(bool)
}
impl ExtnPayloadProvider for CustomRequest {
fn get_from_payload(payload: ExtnPayload) -> Option<Self> {
match payload {
ExtnPayload::Request(r) => match r {
...snip
_ => {}
},
_ => {}
}
None
}
fn get_extn_payload(&self) -> ExtnPayload {
ExtnPayload::Request(ExtnRequest::Custom((
self.clone(),
)))
}
fn contract() -> RippleContract {
RippleContract::Custom
}
}
Once a request is mapped to a contract we can start implementing a processor for the contract. The processor follows similar pattern which requires implementation of couple of traits. Here is the example.
#[derive(Debug)]
pub struct CustomProcessor {
state: CloneableState,
streamer: ExtnStreamer,
}
impl ExtnStreamProcessor for CustomProcessor {
type S = CloneableState;
type V = CustomRequest;
fn get_state(&self) -> Self::S {
self.state.clone()
}
fn sender(&self) -> MSender<ExtnMessage> {
self.streamer.sender()
}
fn receiver(&mut self) -> MReceiver<ExtnMessage> {
self.streamer.receiver()
}
}
#[async_trait]
impl ExtnRequestProcessor for CustomProcessor {
async fn process_error(
_state: Self::S,
_msg: ExtnMessage,
_error: ripple_sdk::utils::error::RippleError,
) -> Option<bool> {
#... snip
}
async fn process_request(state: Self::S,
msg: ExtnMessage,
extracted_message: Self::V,
) -> Option<bool> {
#... snip
}
client.addRequestProcessor(CustomProcessor)
Calling and receiving response is pretty straight forward.
if let Ok(response) = client.request(CustomRequest::Get).await {
if let Some(ExtnResponse::String(v)) = response.payload.clone().extract() {
// Success receiving a String response from the payload
} else {
// Error
}
If calling from an extension we need to make sure the Extn manifest is updated accordingly as detailed here.
Create a new rust repo and setup the cargo.toml like below
[package]
name = "distributor_general"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
# Ripple sdk should be opensourced and available from crates.io at this point
ripple_sdk = "0.8.0"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
Each extension should define the metadata and the builder.
Each extension needs to provide the metadata for its usage and capabilities of the contracts. Here is an example.
/// Defines the contracts fulfilled by this extension. This should match the extension manifest file
fn init_library() -> CExtnMetadata {
let _ = init_logger("distributor_general".into());
let dist_meta = ExtnSymbolMetadata::get(
ExtnId::new_channel(ExtnClassId::Distributor, "general".into()),
ContractFulfiller::new(vec![
RippleContract::Permissions
]),
Version::new(1, 1, 0),
);
debug!("Returning distributor builder");
let extn_metadata = ExtnMetadata {
name: "distributor_general".into(),
symbols: vec![dist_meta],
};
extn_metadata.into()
}
export_extn_metadata!(CExtnMetadata, init_library);
For a channel extension, Extension client needs to be initialized and processors needs to be setup as defined in the Contract Fulfiller.
Here is an example
fn start(sender: ExtnSender, receiver: CReceiver<CExtnMessage>) {
let _ = init_logger("distributor_general".into());
info!("Starting distributor channel");
let runtime = Runtime::new().unwrap();
let mut client = ExtnClient::new(receiver.clone(), sender);
runtime.block_on(async move {
let client_c = client.clone();
tokio::spawn(async move {
client.add_request_processor(DistributorPermissionProcessor::new(client.clone()));
// Lets Main know that the distributor channel is ready
let _ = client.event(ExtnStatus::Ready).await;
});
client_c.initialize().await;
});
}
fn build(extn_id: String) -> Result<Box<ExtnChannel>, RippleError> {
if let Ok(id) = ExtnId::try_from(extn_id.clone()) {
let current_id = ExtnId::new_channel(ExtnClassId::Distributor, "general".into());
if id.eq(¤t_id) {
return Ok(Box::new(ExtnChannel {
start,
}));
} else {
Err(RippleError::ExtnError)
}
} else {
Err(RippleError::InvalidInput)
}
}
fn init_extn_builder() -> ExtnChannelBuilder {
ExtnChannelBuilder {
build,
service: "distributor_general".into(),
}
}
export_channel_builder!(ExtnChannelBuilder, init_extn_builder);
Extension manifest requires the name of the dynamic library and path if not relative. Below is a snapshot of a extension manifest entry for this new extension
{
"path": "libdistributor_general",
"symbols": [
{
"id": "ripple:channel:distributor:general",
"uses": [
"config"
],
"fulfills": [
"permissions"
]
}
]
}