hop-sdk 0.1.0

Client SDK for the Polkadot HOP protocol
Documentation
use std::sync::Arc;

use crate::errors::HopError;
use crate::internal::crypto::{
    generate_keypair, hash, sign, signing_key_from_ticket_secret, SignatureScheme,
};
use crate::internal::resolver::{self, HopEnvironment};
use crate::internal::scale_multi::{encode_multi_signature, encode_multi_signer};
use crate::internal::ticket_internals::{pack_ticket, unpack_hash, unpack_scheme, unpack_secret};
use crate::internal::transport::WsJsonRpcTransport;
use crate::ticket::HopTicket;

const MAX_DATA_SIZE: usize = 64 * 1024 * 1024; // 64 MiB
const SCHEME: SignatureScheme = SignatureScheme::Sr25519;

pub struct HopClient {
    runtime: tokio::runtime::Runtime,
    transport: Arc<WsJsonRpcTransport>,
}

impl HopClient {
    pub fn connect(env: HopEnvironment) -> Result<Self, HopError> {
        let endpoint = resolver::resolve(env)?;

        let runtime = tokio::runtime::Runtime::new()
            .map_err(|e| HopError::Network(format!("Failed to create runtime: {e}")))?;

        let transport = runtime.block_on(async { WsJsonRpcTransport::connect(&endpoint).await })?;

        Ok(Self {
            runtime,
            transport: Arc::new(transport),
        })
    }

    pub fn send(&self, data: Vec<u8>, recipient_count: u32) -> Result<Vec<Arc<HopTicket>>, HopError> {
        if data.is_empty() {
            return Err(HopError::DataTooLarge("Data must not be empty".into()));
        }
        if data.len() > MAX_DATA_SIZE {
            return Err(HopError::DataTooLarge(format!(
                "Data is {} bytes, max is {} bytes (64 MiB)",
                data.len(),
                MAX_DATA_SIZE
            )));
        }

        let count = recipient_count as usize;

        let keypairs: Vec<_> = (0..count).map(|_| generate_keypair(SCHEME)).collect();

        let encoded_keys: Vec<Vec<u8>> = keypairs
            .iter()
            .map(|kp| encode_multi_signer(&kp.public_key, kp.scheme))
            .collect();

        let proof = Vec::new();

        self.runtime.block_on(async {
            self.transport.submit(&data, &encoded_keys, &proof).await
        })?;

        let data_hash = hash(&data);

        let tickets: Vec<Arc<HopTicket>> = keypairs
            .iter()
            .map(|kp| {
                let mut secret = [0u8; 32];
                secret.copy_from_slice(&kp.ticket_secret);
                let raw = pack_ticket(kp.scheme, &data_hash, &secret);
                Arc::new(HopTicket::from_raw(raw))
            })
            .collect();

        Ok(tickets)
    }

    pub fn claim(&self, ticket: Arc<HopTicket>) -> Result<Vec<u8>, HopError> {
        let raw = ticket.raw();
        let scheme = unpack_scheme(raw)
            .ok_or_else(|| HopError::InvalidTicket("Unknown scheme byte".into()))?;
        let data_hash = unpack_hash(raw);
        let ticket_secret = unpack_secret(raw);

        let signing_key = signing_key_from_ticket_secret(&ticket_secret, scheme);
        let raw_sig = sign(&data_hash, &signing_key, scheme);
        let signature = encode_multi_signature(&raw_sig, scheme);

        self.runtime
            .block_on(async { self.transport.claim(&data_hash, &signature).await })
    }

    pub fn destroy(&self) {
        self.runtime.block_on(async {
            self.transport.close().await;
        });
    }
}