use std::fmt;
use std::future::Future;
use std::pin::Pin;
use ::tendermint::chain::Id;
use celestia_proto::cosmos::tx::v1beta1::SignDoc;
use k256::ecdsa::signature::Signer;
use k256::ecdsa::VerifyingKey;
use prost::{Message, Name};
use tendermint_proto::google::protobuf::Any;
use tendermint_proto::Protobuf;
use celestia_proto::cosmos::crypto::secp256k1;
use celestia_types::state::auth::BaseAccount;
use celestia_types::state::{AuthInfo, Fee, ModeInfo, RawTx, RawTxBody, SignerInfo, Sum};
use crate::Result;
pub type DocSignature = k256::ecdsa::Signature;
pub type SignatureError = k256::ecdsa::signature::Error;
pub(crate) struct BoxedDocSigner(Box<dyn AbstractDocSigner>);
pub trait DocSigner: Send + Sync {
fn try_sign(
&self,
doc: SignDoc,
) -> impl Future<Output = Result<DocSignature, SignatureError>> + Send;
}
impl<T> DocSigner for T
where
T: Signer<DocSignature> + Send + Sync + 'static,
{
async fn try_sign(&self, doc: SignDoc) -> Result<DocSignature, SignatureError> {
let bytes = doc.encode_to_vec();
self.try_sign(&bytes)
}
}
impl BoxedDocSigner {
pub fn new<S>(signer: S) -> BoxedDocSigner
where
S: AbstractDocSigner,
{
BoxedDocSigner(Box::new(signer))
}
}
impl DocSigner for BoxedDocSigner {
async fn try_sign(&self, doc: SignDoc) -> Result<DocSignature, SignatureError> {
self.0.try_sign(doc).await
}
}
pub(crate) trait AbstractDocSigner: Sync + Send + 'static {
fn try_sign<'a>(
&'a self,
doc: SignDoc,
) -> Pin<Box<dyn Future<Output = Result<DocSignature, SignatureError>> + Send + 'a>>;
}
impl<T> AbstractDocSigner for T
where
T: DocSigner + Sync + Send + 'static,
{
fn try_sign<'a>(
&'a self,
doc: SignDoc,
) -> Pin<Box<dyn Future<Output = Result<DocSignature, SignatureError>> + Send + 'a>> {
Box::pin(DocSigner::try_sign(self, doc))
}
}
impl fmt::Debug for BoxedDocSigner {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("BoxedDocSigner { .. }")
}
}
pub async fn sign_tx(
tx_body: RawTxBody,
chain_id: Id,
base_account: &BaseAccount,
verifying_key: &VerifyingKey,
signer: &impl DocSigner,
gas_limit: u64,
fee: u64,
) -> Result<RawTx> {
const SIGNING_MODE_INFO: ModeInfo = ModeInfo {
sum: Sum::Single { mode: 1 },
};
let public_key = secp256k1::PubKey {
key: verifying_key.to_encoded_point(true).as_bytes().to_vec(),
};
let public_key_as_any = Any {
type_url: secp256k1::PubKey::type_url(),
value: public_key.encode_to_vec(),
};
let mut fee = Fee::new(fee, gas_limit);
fee.payer = Some(base_account.address.clone().into());
let auth_info = AuthInfo {
signer_infos: vec![SignerInfo {
public_key: Some(public_key_as_any),
mode_info: SIGNING_MODE_INFO,
sequence: base_account.sequence,
}],
fee,
};
let doc = SignDoc {
body_bytes: tx_body.encode_to_vec(),
auth_info_bytes: auth_info.clone().encode_vec(),
chain_id: chain_id.into(),
account_number: base_account.account_number,
};
let signature = signer.try_sign(doc).await?;
Ok(RawTx {
auth_info: Some(auth_info.into()),
body: Some(tx_body),
signatures: vec![signature.to_bytes().to_vec()],
})
}
#[cfg(feature = "uniffi")]
pub use uniffi_types::*;
#[cfg(feature = "uniffi")]
mod uniffi_types {
use super::*;
use async_trait::async_trait;
use k256::ecdsa::signature::Error as K256Error;
use std::sync::Arc;
use tendermint::signature::Secp256k1Signature;
use uniffi::Record;
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum SignerError {
#[error("uniffi conversion error: {msg}")]
UniffiConversionError {
msg: String,
},
#[error("error while signing: {msg}")]
SigningError {
msg: String,
},
}
#[uniffi::export(with_foreign)]
#[async_trait]
pub trait UniffiSigner: Sync + Send {
async fn sign(&self, doc: SignDoc) -> Result<UniffiSignature, SignerError>;
}
pub struct UniffiSignerBox(pub Arc<dyn UniffiSigner>);
#[derive(Record)]
pub struct UniffiSignature {
pub bytes: Vec<u8>,
}
impl DocSigner for UniffiSignerBox {
async fn try_sign(&self, doc: SignDoc) -> Result<Secp256k1Signature, K256Error> {
match self.0.sign(doc).await {
Ok(s) => Secp256k1Signature::try_from(s).map_err(K256Error::from_source),
Err(e) => Err(K256Error::from_source(e)),
}
}
}
impl From<DocSignature> for UniffiSignature {
fn from(value: DocSignature) -> Self {
UniffiSignature {
bytes: value.to_vec(),
}
}
}
impl TryFrom<UniffiSignature> for DocSignature {
type Error = SignerError;
fn try_from(value: UniffiSignature) -> std::result::Result<Self, Self::Error> {
DocSignature::from_slice(&value.bytes).map_err(|e| SignerError::SigningError {
msg: format!("invalid signature {e}"),
})
}
}
}
#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
pub use wbg::*;
#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
mod wbg {
use super::*;
use celestia_proto::cosmos::tx::v1beta1::SignDoc;
use js_sys::{BigInt, Function, Promise, Uint8Array};
use lumina_utils::make_object;
use send_wrapper::SendWrapper;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
#[wasm_bindgen(js_name = protoEncodeSignDoc)]
pub fn proto_encode_sign_doc(sign_doc: JsSignDoc) -> Vec<u8> {
SignDoc::from(sign_doc).encode_to_vec()
}
pub struct JsSigner {
signer_fn: SendWrapper<JsSignerFn>,
}
impl JsSigner {
pub fn new(function: JsSignerFn) -> Self {
Self {
signer_fn: SendWrapper::new(function),
}
}
}
impl DocSigner for JsSigner {
async fn try_sign(&self, doc: SignDoc) -> Result<DocSignature, SignatureError> {
let promise = {
let msg = JsSignDoc::from(doc);
let sig_or_promise = self.signer_fn.call1(&JsValue::null(), &msg).map_err(|e| {
let err = format!("Error calling signer fn: {e:?}");
SignatureError::from_source(err)
})?;
if !sig_or_promise.has_type::<Promise>() {
return try_into_signature(sig_or_promise);
}
sig_or_promise.unchecked_into::<Promise>()
};
let sig = SendWrapper::new(JsFuture::from(promise))
.await
.map_err(|e| {
let err = format!("Error awaiting signer promise: {e:?}");
SignatureError::from_source(err)
})?;
try_into_signature(sig)
}
}
fn try_into_signature(val: JsValue) -> Result<DocSignature, SignatureError> {
let sig = val.dyn_into::<Uint8Array>().map_err(|orig| {
let err = format!(
"Signature must be Uint8Array, found: {}",
orig.js_typeof().as_string().expect("typeof returns string")
);
SignatureError::from_source(err)
})?;
DocSignature::from_slice(&sig.to_vec()).map_err(SignatureError::from_source)
}
#[wasm_bindgen(typescript_custom_section)]
const _: &str = "
/**
* A payload to be signed
*/
export interface SignDoc {
bodyBytes: Uint8Array;
authInfoBytes: Uint8Array;
chainId: string;
accountNumber: bigint;
}
/**
* A function that produces a signature of a payload
*/
export type SignerFn = ((arg: SignDoc) => Uint8Array) | ((arg: SignDoc) => Promise<Uint8Array>);
";
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(extends = Function, typescript_type = "SignerFn")]
pub type JsSignerFn;
#[wasm_bindgen(typescript_type = "SignDoc")]
pub type JsSignDoc;
#[wasm_bindgen(method, getter, js_name = bodyBytes)]
pub fn body_bytes(this: &JsSignDoc) -> Vec<u8>;
#[wasm_bindgen(method, getter, js_name = authInfoBytes)]
pub fn auth_info_bytes(this: &JsSignDoc) -> Vec<u8>;
#[wasm_bindgen(method, getter, js_name = chainId)]
pub fn chain_id(this: &JsSignDoc) -> String;
#[wasm_bindgen(method, getter, js_name = accountNumber)]
pub fn account_number(this: &JsSignDoc) -> u64;
}
impl From<JsSignDoc> for SignDoc {
fn from(value: JsSignDoc) -> SignDoc {
SignDoc {
body_bytes: value.body_bytes(),
auth_info_bytes: value.auth_info_bytes(),
chain_id: value.chain_id(),
account_number: value.account_number(),
}
}
}
impl From<SignDoc> for JsSignDoc {
fn from(value: SignDoc) -> JsSignDoc {
let obj = make_object!(
"bodyBytes" => Uint8Array::from(value.body_bytes.as_ref()),
"authInfoBytes" => Uint8Array::from(value.auth_info_bytes.as_ref()),
"chainId" => value.chain_id.into(),
"accountNumber" => BigInt::from(value.account_number)
);
obj.unchecked_into()
}
}
}