Skip to main content

tempo_alloy/provider/
ext.rs

1use alloy_contract::Result as ContractResult;
2use alloy_primitives::{Address, U256};
3use alloy_provider::{
4    Identity, Provider, ProviderBuilder,
5    fillers::{JoinFill, RecommendedFillers},
6};
7use tempo_contracts::precompiles::{
8    ACCOUNT_KEYCHAIN_ADDRESS,
9    IAccountKeychain::{IAccountKeychainInstance, KeyInfo},
10};
11
12use crate::{
13    TempoFillers, TempoNetwork,
14    fillers::{ExpiringNonceFiller, NonceKeyFiller, Random2DNonceFiller},
15};
16
17/// Extension trait for [`Provider`] with Tempo-specific functionality.
18#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
19#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
20pub trait TempoProviderExt: Provider<TempoNetwork> {
21    /// Returns a typed instance for the Account Keychain precompile.
22    fn account_keychain(&self) -> IAccountKeychainInstance<&Self, TempoNetwork>
23    where
24        Self: Sized,
25    {
26        IAccountKeychainInstance::new(ACCOUNT_KEYCHAIN_ADDRESS, self)
27    }
28
29    /// Returns information about a key authorized for an account.
30    async fn get_keychain_key(&self, account: Address, key_id: Address) -> ContractResult<KeyInfo>
31    where
32        Self: Sized,
33    {
34        self.account_keychain().getKey(account, key_id).call().await
35    }
36
37    /// Returns the remaining spending limit for an account/key/token tuple.
38    async fn get_keychain_remaining_limit(
39        &self,
40        account: Address,
41        key_id: Address,
42        token: Address,
43    ) -> ContractResult<U256>
44    where
45        Self: Sized,
46    {
47        self.account_keychain()
48            .getRemainingLimit(account, key_id, token)
49            .call()
50            .await
51    }
52
53    /// Returns the key ID used in the current transaction context.
54    async fn get_keychain_transaction_key(&self) -> ContractResult<Address>
55    where
56        Self: Sized,
57    {
58        self.account_keychain().getTransactionKey().call().await
59    }
60}
61
62#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
63#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
64impl<P> TempoProviderExt for P where P: Provider<TempoNetwork> {}
65
66/// Extension trait for [`ProviderBuilder`] with Tempo-specific functionality.
67pub trait TempoProviderBuilderExt {
68    /// Returns a provider builder with the recommended Tempo fillers and the random 2D nonce filler.
69    ///
70    /// See [`Random2DNonceFiller`] for more information on random 2D nonces.
71    fn with_random_2d_nonces(
72        self,
73    ) -> ProviderBuilder<
74        Identity,
75        JoinFill<Identity, TempoFillers<Random2DNonceFiller>>,
76        TempoNetwork,
77    >;
78
79    /// Returns a provider builder with the recommended Tempo fillers and the expiring nonce filler.
80    ///
81    /// See [`ExpiringNonceFiller`] for more information on expiring nonces ([TIP-1009]).
82    ///
83    /// [TIP-1009]: <https://docs.tempo.xyz/protocol/tips/tip-1009>
84    fn with_expiring_nonces(
85        self,
86    ) -> ProviderBuilder<
87        Identity,
88        JoinFill<Identity, TempoFillers<ExpiringNonceFiller>>,
89        TempoNetwork,
90    >;
91
92    /// Returns a provider builder with the recommended Tempo fillers and the nonce key filler.
93    ///
94    /// The nonce key filler requires `nonce_key` to be set on the transaction request and
95    /// fills the correct next nonce by querying the chain, with caching for batched sends.
96    ///
97    /// See [`NonceKeyFiller`] for more information.
98    fn with_nonce_key_filler(
99        self,
100    ) -> ProviderBuilder<Identity, JoinFill<Identity, TempoFillers<NonceKeyFiller>>, TempoNetwork>;
101}
102
103impl TempoProviderBuilderExt
104    for ProviderBuilder<
105        Identity,
106        JoinFill<Identity, <TempoNetwork as RecommendedFillers>::RecommendedFillers>,
107        TempoNetwork,
108    >
109{
110    fn with_random_2d_nonces(
111        self,
112    ) -> ProviderBuilder<
113        Identity,
114        JoinFill<Identity, TempoFillers<Random2DNonceFiller>>,
115        TempoNetwork,
116    > {
117        ProviderBuilder::default().filler(TempoFillers::default())
118    }
119
120    fn with_expiring_nonces(
121        self,
122    ) -> ProviderBuilder<
123        Identity,
124        JoinFill<Identity, TempoFillers<ExpiringNonceFiller>>,
125        TempoNetwork,
126    > {
127        ProviderBuilder::default().filler(TempoFillers::default())
128    }
129
130    fn with_nonce_key_filler(
131        self,
132    ) -> ProviderBuilder<Identity, JoinFill<Identity, TempoFillers<NonceKeyFiller>>, TempoNetwork>
133    {
134        ProviderBuilder::default().filler(TempoFillers::default())
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use alloy::sol_types::SolCall;
141    use alloy_primitives::{Address, Bytes, U256};
142    use alloy_provider::{Identity, ProviderBuilder, fillers::JoinFill, mock::Asserter};
143    use tempo_contracts::precompiles::IAccountKeychain::{
144        KeyInfo, SignatureType, getKeyCall, getRemainingLimitCall, getTransactionKeyCall,
145    };
146
147    use crate::{
148        TempoFillers, TempoNetwork,
149        fillers::{ExpiringNonceFiller, NonceKeyFiller, Random2DNonceFiller},
150        provider::ext::{TempoProviderBuilderExt, TempoProviderExt},
151    };
152
153    fn mock_provider(asserter: Asserter) -> impl alloy_provider::Provider<TempoNetwork> {
154        ProviderBuilder::<_, _, TempoNetwork>::default().connect_mocked_client(asserter)
155    }
156
157    #[test]
158    fn test_with_random_nonces() {
159        let _: ProviderBuilder<_, JoinFill<Identity, TempoFillers<Random2DNonceFiller>>, _> =
160            ProviderBuilder::new_with_network::<TempoNetwork>().with_random_2d_nonces();
161    }
162
163    #[test]
164    fn test_with_expiring_nonces() {
165        let _: ProviderBuilder<_, JoinFill<Identity, TempoFillers<ExpiringNonceFiller>>, _> =
166            ProviderBuilder::new_with_network::<TempoNetwork>().with_expiring_nonces();
167    }
168
169    #[test]
170    fn test_with_nonce_key_filler() {
171        let _: ProviderBuilder<_, JoinFill<Identity, TempoFillers<NonceKeyFiller>>, _> =
172            ProviderBuilder::new_with_network::<TempoNetwork>().with_nonce_key_filler();
173    }
174
175    #[tokio::test]
176    async fn test_get_keychain_key() {
177        let asserter = Asserter::new();
178        let provider = mock_provider(asserter.clone());
179        let account = Address::repeat_byte(0x11);
180        let key_id = Address::repeat_byte(0x22);
181        let expected = KeyInfo {
182            signatureType: SignatureType::P256,
183            keyId: key_id,
184            expiry: 1_234_567_890,
185            enforceLimits: true,
186            isRevoked: false,
187        };
188
189        asserter.push_success(&Bytes::from(getKeyCall::abi_encode_returns(&expected)));
190
191        let actual = provider
192            .get_keychain_key(account, key_id)
193            .await
194            .expect("key info call succeeds");
195
196        assert_eq!(actual, expected);
197    }
198
199    #[tokio::test]
200    async fn test_get_keychain_remaining_limit() {
201        let asserter = Asserter::new();
202        let provider = mock_provider(asserter.clone());
203        let account = Address::repeat_byte(0x11);
204        let key_id = Address::repeat_byte(0x22);
205        let token = Address::repeat_byte(0x33);
206        let expected = U256::from(42_u64);
207
208        asserter.push_success(&Bytes::from(getRemainingLimitCall::abi_encode_returns(
209            &expected,
210        )));
211
212        let actual = provider
213            .get_keychain_remaining_limit(account, key_id, token)
214            .await
215            .expect("remaining limit call succeeds");
216
217        assert_eq!(actual, expected);
218    }
219
220    #[tokio::test]
221    async fn test_get_keychain_transaction_key() {
222        let asserter = Asserter::new();
223        let provider = mock_provider(asserter.clone());
224        let expected = Address::repeat_byte(0x44);
225
226        asserter.push_success(&Bytes::from(getTransactionKeyCall::abi_encode_returns(
227            &expected,
228        )));
229
230        let actual = provider
231            .get_keychain_transaction_key()
232            .await
233            .expect("transaction key call succeeds");
234
235        assert_eq!(actual, expected);
236    }
237
238    #[tokio::test]
239    async fn test_account_keychain_accessor() {
240        let asserter = Asserter::new();
241        let provider = mock_provider(asserter.clone());
242        let account = Address::repeat_byte(0x11);
243        let key_id = Address::repeat_byte(0x22);
244        let expected = KeyInfo {
245            signatureType: SignatureType::Secp256k1,
246            keyId: key_id,
247            expiry: u64::MAX,
248            enforceLimits: false,
249            isRevoked: true,
250        };
251
252        asserter.push_success(&Bytes::from(getKeyCall::abi_encode_returns(&expected)));
253
254        let actual = provider
255            .account_keychain()
256            .getKey(account, key_id)
257            .call()
258            .await
259            .expect("typed instance call succeeds");
260
261        assert_eq!(actual, expected);
262    }
263
264    #[tokio::test]
265    async fn test_get_keychain_key_propagates_errors() {
266        let asserter = Asserter::new();
267        let provider = mock_provider(asserter.clone());
268
269        asserter.push_failure_msg("boom");
270
271        let err = provider
272            .get_keychain_key(Address::repeat_byte(0x11), Address::repeat_byte(0x22))
273            .await
274            .expect_err("errors should propagate");
275
276        assert!(matches!(err, alloy_contract::Error::TransportError(_)));
277        assert!(err.to_string().contains("boom"));
278    }
279}