use celestia_proto::cosmos::tx::v1beta1::SignDoc;
use celestia_types::any::JsAny;
use celestia_types::consts::appconsts::JsAppVersion;
use celestia_types::state::auth::{JsAuthParams, JsBaseAccount};
use celestia_types::state::JsCoin;
use celestia_types::Blob;
use js_sys::{BigInt, Function, Promise, Uint8Array};
use k256::ecdsa::signature::Error as SignatureError;
use k256::ecdsa::{Signature, VerifyingKey};
use lumina_utils::make_object;
use prost::Message;
use tonic_web_wasm_client::Client;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use crate::grpc::TxPriority;
use crate::tx::{DocSigner, JsTxConfig, JsTxInfo};
use crate::{Result, TxClient};
#[wasm_bindgen(js_name = "TxClient")]
pub struct JsTxClient {
client: TxClient<Client, JsSigner>,
}
#[wasm_bindgen(js_class = "TxClient")]
impl JsTxClient {
#[wasm_bindgen(constructor)]
pub async fn new(url: &str, pubkey: Uint8Array, signer_fn: JsSignerFn) -> Result<JsTxClient> {
let signer = JsSigner { signer_fn };
let pubkey = VerifyingKey::try_from(pubkey.to_vec().as_slice())?;
let client = TxClient::with_grpcweb_url(url, pubkey, signer).await?;
Ok(Self { client })
}
#[wasm_bindgen(js_name = minGasPrice)]
pub async fn min_gas_price(&self) -> Result<f64> {
self.client.get_min_gas_price().await
}
#[wasm_bindgen(js_name = getEstimateGasPrice)]
pub async fn estimate_gas_price(&self, priority: TxPriority) -> Result<f64> {
self.client.estimate_gas_price(priority).await
}
#[wasm_bindgen(js_name = chainId, getter)]
pub fn chain_id(&self) -> String {
self.client.chain_id().to_string()
}
#[wasm_bindgen(js_name = appVersion, getter)]
pub fn app_version(&self) -> JsAppVersion {
self.client.app_version().into()
}
#[wasm_bindgen(js_name = submitBlobs)]
pub async fn submit_blobs(
&self,
blobs: Vec<Blob>,
tx_config: Option<JsTxConfig>,
) -> Result<JsTxInfo> {
let tx_config = tx_config.map(Into::into).unwrap_or_default();
let tx = self.client.submit_blobs(&blobs, tx_config).await?;
Ok(tx.into())
}
#[wasm_bindgen(js_name = submitMessage)]
pub async fn submit_message(
&self,
message: JsAny,
tx_config: Option<JsTxConfig>,
) -> Result<JsTxInfo> {
let tx_config = tx_config.map(Into::into).unwrap_or_default();
let tx = self.client.submit_message(message, tx_config).await?;
Ok(tx.into())
}
#[wasm_bindgen(js_name = getAuthParams)]
pub async fn get_auth_params(&self) -> Result<JsAuthParams> {
self.client.get_auth_params().await.map(Into::into)
}
#[wasm_bindgen(js_name = getAccount)]
pub async fn get_account(&self, account: &str) -> Result<JsBaseAccount> {
self.client
.get_account(&account.parse()?)
.await
.map(Into::into)
}
#[wasm_bindgen(js_name = getAccounts)]
pub async fn get_accounts(&self) -> Result<Vec<JsBaseAccount>> {
self.client
.get_accounts()
.await
.map(|accs| accs.into_iter().map(Into::into).collect())
}
#[wasm_bindgen(js_name = getBalance)]
pub async fn get_balance(&self, address: &str, denom: &str) -> Result<JsCoin> {
self.client
.get_balance(&address.parse()?, denom)
.await
.map(Into::into)
}
#[wasm_bindgen(js_name = getAllBalances)]
pub async fn get_all_balances(&self, address: &str) -> Result<Vec<JsCoin>> {
self.client
.get_all_balances(&address.parse()?)
.await
.map(|coins| coins.into_iter().map(Into::into).collect())
}
#[wasm_bindgen(js_name = getSpendableBalances)]
pub async fn get_spendable_balances(&self, address: &str) -> Result<Vec<JsCoin>> {
self.client
.get_spendable_balances(&address.parse()?)
.await
.map(|coins| coins.into_iter().map(Into::into).collect())
}
#[wasm_bindgen(js_name = getTotalSupply)]
pub async fn get_total_supply(&self) -> Result<Vec<JsCoin>> {
self.client
.get_total_supply()
.await
.map(|coins| coins.into_iter().map(Into::into).collect())
}
}
#[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: JsSignerFn,
}
impl DocSigner for JsSigner {
async fn try_sign(&self, doc: SignDoc) -> Result<Signature, SignatureError> {
let msg = JsSignDoc::from(doc);
let mut res = self.signer_fn.call1(&JsValue::null(), &msg).map_err(|e| {
let err = format!("Error calling signer fn: {e:?}");
SignatureError::from_source(err)
})?;
if res.has_type::<Promise>() {
let promise = res.unchecked_into::<Promise>();
res = JsFuture::from(promise).await.map_err(|e| {
let err = format!("Error awaiting signer promise: {e:?}");
SignatureError::from_source(err)
})?
}
let sig = res.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)
})?;
Signature::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()
}
}