wallet_adapter/wallet_ser_der/standard_features/
sign_tx.rs

1use ed25519_dalek::Signature;
2use wallet_adapter_common::{clusters::Cluster, WalletCommonUtils};
3use web_sys::{
4    js_sys::{self, Function},
5    wasm_bindgen::JsValue,
6};
7
8use core::hash::Hash;
9
10use crate::{Commitment, Reflection, SemverVersion, WalletAccount, WalletError, WalletResult};
11
12/// Used in `solana:SignTransaction` and `solana:SignAndSendTransaction`.
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct SignTransaction {
15    /// The [semver version](SemverVersion) of the
16    /// callback function supported by the wallet
17    pub version: SemverVersion,
18    /// Whether the wallet supports signing legacy transactions.
19    /// If a wallet does not support this an error is returned
20    pub legacy: bool,
21    /// Whether the wallet supports signing versioned transactions
22    pub version_zero: bool,
23    // Internally called. Can be either `solana:signTransaction`
24    // or `solana:signAndSendTransaction` callback function
25    callback: Function,
26}
27
28impl SignTransaction {
29    fn new(reflection: &Reflection, version: SemverVersion, key: &str) -> WalletResult<Self> {
30        let inner_value = reflection
31            .reflect_inner(key)
32            .or(Err(WalletError::MissingSignTransactionFunction))?;
33        let callback = Reflection::new(inner_value)?
34            .into_function()
35            .map_err(|error| {
36                WalletError::InternalError(format!("Namespace[`solana:{key}`]: {error}"))
37            })?;
38
39        let (legacy, version_zero) = Self::get_tx_version_support(reflection)?;
40
41        Ok(Self {
42            version,
43            callback,
44            legacy,
45            version_zero,
46        })
47    }
48
49    /// Parse a `solana:signTransaction` callback from the [JsValue]
50    pub(crate) fn new_sign_tx(
51        reflection: &Reflection,
52        version: SemverVersion,
53    ) -> WalletResult<Self> {
54        Self::new(reflection, version, "signTransaction")
55    }
56
57    /// Parse a `solana:signAndSendTransaction` callback from the [JsValue]
58    pub(crate) fn new_sign_and_send_tx(
59        reflection: &Reflection,
60        version: SemverVersion,
61    ) -> WalletResult<Self> {
62        Self::new(reflection, version, "signAndSendTransaction")
63    }
64
65    fn get_tx_version_support(inner_value: &Reflection) -> WalletResult<(bool, bool)> {
66        let tx_version_support_jsvalue = inner_value
67            .reflect_inner("supportedTransactionVersions")
68            .or(Err(WalletError::ExpectedValueNotFound(
69                "supportedTransactionVersions".to_string(),
70            )))?;
71        let tx_version_support = Reflection::new(tx_version_support_jsvalue)?.into_array()?;
72
73        let mut legacy = false;
74        let mut version_zero = false;
75
76        tx_version_support.iter().try_for_each(|value| {
77            if value == JsValue::from_str("legacy") {
78                legacy = true;
79            } else if value == 0 {
80                version_zero = true;
81            } else {
82                return Err(WalletError::UnsupportedTransactionVersion);
83            }
84
85            Ok(())
86        })?;
87
88        if !legacy {
89            return Err(WalletError::LegacyTransactionSupportRequired);
90        }
91
92        Ok((legacy, version_zero))
93    }
94
95    pub(crate) async fn call_sign_tx(
96        &self,
97        wallet_account: &WalletAccount,
98        transaction_bytes: &[u8],
99        cluster: Option<Cluster>,
100    ) -> WalletResult<Vec<Vec<u8>>> {
101        let tx_bytes_value: js_sys::Uint8Array = transaction_bytes.into();
102
103        let mut tx_object = Reflection::new_object();
104        tx_object.set_object(&"account".into(), &wallet_account.js_value)?;
105        tx_object.set_object(&"transaction".into(), &tx_bytes_value)?;
106        if let Some(cluster) = cluster {
107            tx_object.set_object(&"chain".into(), &cluster.chain().into())?;
108        }
109
110        let outcome = self.callback.call1(&JsValue::null(), &tx_object.take())?;
111
112        let outcome = js_sys::Promise::resolve(&outcome);
113
114        let success = wasm_bindgen_futures::JsFuture::from(outcome).await?;
115        Reflection::new(success)?.get_bytes_from_vec("signedTransaction")
116    }
117
118    pub(crate) async fn call_sign_and_send_transaction(
119        &self,
120        wallet_account: &WalletAccount,
121        transaction_bytes: &[u8],
122        cluster: Cluster,
123        options: SendOptions,
124    ) -> WalletResult<Signature> {
125        let tx_bytes_value: js_sys::Uint8Array = transaction_bytes.into();
126
127        let mut tx_object = Reflection::new_object();
128        tx_object.set_object(&"account".into(), &wallet_account.js_value)?;
129        tx_object.set_object(&"transaction".into(), &tx_bytes_value)?;
130        tx_object.set_object(&"chain".into(), &cluster.chain().into())?;
131        tx_object.set_object(&"options".into(), &options.to_object()?)?;
132
133        let outcome = self.callback.call1(&JsValue::null(), &tx_object.take())?;
134
135        let outcome = js_sys::Promise::resolve(&outcome);
136
137        let success = wasm_bindgen_futures::JsFuture::from(outcome).await?;
138
139        Reflection::new(success)?
140            .get_bytes_from_vec("signature")?
141            .first()
142            .map(|value| {
143                let bytes = WalletCommonUtils::to64byte_array(value)?;
144                Ok(WalletCommonUtils::signature(&bytes))
145            })
146            .ok_or(WalletError::SendAndSignTransactionSignatureEmpty)?
147    }
148}
149
150#[allow(clippy::non_canonical_partial_ord_impl)]
151impl PartialOrd for SignTransaction {
152    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
153        Some(self.version.cmp(&other.version))
154    }
155}
156
157impl Ord for SignTransaction {
158    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
159        self.version.cmp(&other.version)
160    }
161}
162
163impl Hash for SignTransaction {
164    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
165        self.version.hash(state);
166    }
167}
168
169/// Options used in the `solana:signAndSendTransaction` method
170/// on a [crate::Wallet]. These options are:
171/// - [preflight_commitment](Commitment)
172/// - [skip_preflight](bool)
173/// - [max_retries](u8)
174#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
175pub struct SendOptions {
176    preflight_commitment: Commitment,
177    skip_preflight: bool,
178    max_retries: u8,
179}
180
181impl SendOptions {
182    /// Converts [SendOptions] to a [JsValue] which can be passed
183    /// to the browser wallet when making requests.
184    /// Internally, it is a [js_sys::Object]
185    pub fn to_object(&self) -> WalletResult<JsValue> {
186        let mut reflection = Reflection::new_object();
187        reflection.set_object_str("preflightCommitment", self.preflight_commitment.as_str())?;
188        reflection.set_object(&"skipPreflight".into(), &JsValue::from(self.skip_preflight))?;
189        reflection.set_object(&"maxRetries".into(), &JsValue::from(self.max_retries))?;
190
191        Ok(reflection.take())
192    }
193}