penumbra_sdk_keys/address/
view.rs

1use std::cmp::Ordering;
2
3use serde::{Deserialize, Serialize};
4
5use penumbra_sdk_proto::{penumbra::core::keys::v1 as pb, DomainType};
6
7use crate::keys::{AddressIndex, WalletId};
8
9use super::Address;
10
11/// A view of a Penumbra address, either an opaque payment address or an address
12/// with known structure.
13///
14/// This type allows working with addresses and address indexes without knowing
15/// the corresponding FVK.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(try_from = "pb::AddressView", into = "pb::AddressView")]
18pub enum AddressView {
19    Opaque {
20        address: Address,
21    },
22    Decoded {
23        address: Address,
24        index: AddressIndex,
25        wallet_id: WalletId,
26    },
27}
28
29impl AddressView {
30    pub fn address(&self) -> Address {
31        match self {
32            AddressView::Opaque { address } => address.clone(),
33            AddressView::Decoded { address, .. } => address.clone(),
34        }
35    }
36}
37
38impl DomainType for AddressView {
39    type Proto = pb::AddressView;
40}
41
42impl From<AddressView> for pb::AddressView {
43    fn from(view: AddressView) -> Self {
44        match view {
45            AddressView::Opaque { address } => Self {
46                address_view: Some(pb::address_view::AddressView::Opaque(
47                    pb::address_view::Opaque {
48                        address: Some(address.into()),
49                    },
50                )),
51            },
52            AddressView::Decoded {
53                address,
54                index,
55                wallet_id,
56            } => Self {
57                address_view: Some(pb::address_view::AddressView::Decoded(
58                    pb::address_view::Decoded {
59                        address: Some(address.into()),
60                        index: Some(index.into()),
61                        wallet_id: Some(wallet_id.into()),
62                    },
63                )),
64            },
65        }
66    }
67}
68
69impl TryFrom<pb::AddressView> for AddressView {
70    type Error = anyhow::Error;
71    fn try_from(value: pb::AddressView) -> Result<Self, Self::Error> {
72        match value.address_view {
73            Some(pb::address_view::AddressView::Opaque(opaque)) => {
74                let address = opaque
75                    .address
76                    .ok_or_else(|| anyhow::anyhow!("AddressView::Opaque missing address field"))?
77                    .try_into()?;
78                Ok(AddressView::Opaque { address })
79            }
80            Some(pb::address_view::AddressView::Decoded(visible)) => {
81                let address = visible
82                    .address
83                    .ok_or_else(|| anyhow::anyhow!("AddressView::Visible missing address field"))?
84                    .try_into()?;
85                let index = visible
86                    .index
87                    .ok_or_else(|| anyhow::anyhow!("AddressView::Visible missing index field"))?
88                    .try_into()?;
89                let wallet_id = visible
90                    .wallet_id
91                    .ok_or_else(|| anyhow::anyhow!("AddressView::Visible missing wallet_id field"))?
92                    .try_into()?;
93                Ok(AddressView::Decoded {
94                    address,
95                    index,
96                    wallet_id,
97                })
98            }
99            None => Err(anyhow::anyhow!("AddressView missing address_view field")),
100        }
101    }
102}
103
104// Canonical ordering for serialization
105impl PartialOrd for AddressView {
106    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
107        use AddressView::*;
108        // Opaque < Decoded
109        match (self, other) {
110            (Opaque { .. }, Decoded { .. }) => Some(Ordering::Less),
111            (Decoded { .. }, Opaque { .. }) => Some(Ordering::Greater),
112            (Opaque { address: a1 }, Opaque { address: a2 }) => a1.partial_cmp(a2),
113            (
114                Decoded {
115                    address: a1,
116                    index: i1,
117                    wallet_id: w1,
118                },
119                Decoded {
120                    address: a2,
121                    index: i2,
122                    wallet_id: w2,
123                },
124            ) => (a1, i1, w1).partial_cmp(&(a2, i2, w2)),
125        }
126    }
127}
128
129impl Ord for AddressView {
130    fn cmp(&self, other: &Self) -> Ordering {
131        // Opaque < Decoded
132        match (self, other) {
133            (AddressView::Opaque { address: a1 }, AddressView::Opaque { address: a2 }) => {
134                a1.cmp(a2)
135            }
136            (
137                AddressView::Decoded {
138                    address: a1,
139                    index: i1,
140                    wallet_id: w1,
141                },
142                AddressView::Decoded {
143                    address: a2,
144                    index: i2,
145                    wallet_id: w2,
146                },
147            ) => match a1.cmp(a2) {
148                Ordering::Equal => match i1.cmp(i2) {
149                    Ordering::Equal => w1.cmp(w2),
150                    ord => ord,
151                },
152                ord => ord,
153            },
154            (
155                AddressView::Opaque { address: _ },
156                AddressView::Decoded {
157                    address: _,
158                    index: _,
159                    wallet_id: _,
160                },
161            ) => Ordering::Less,
162            (
163                AddressView::Decoded {
164                    address: _,
165                    index: _,
166                    wallet_id: _,
167                },
168                AddressView::Opaque { address: _ },
169            ) => Ordering::Greater,
170        }
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use rand_core::OsRng;
177
178    use crate::keys::{Bip44Path, SeedPhrase, SpendKey};
179
180    use super::*;
181
182    #[test]
183    fn address_view_basic() {
184        let sk1 = SpendKey::from_seed_phrase_bip44(SeedPhrase::generate(OsRng), &Bip44Path::new(0));
185        let sk2 = SpendKey::from_seed_phrase_bip44(SeedPhrase::generate(OsRng), &Bip44Path::new(0));
186
187        let fvk1 = sk1.full_viewing_key();
188        let fvk2 = sk2.full_viewing_key();
189
190        let addr1_0 = fvk1.payment_address(0.into()).0;
191        let addr1_1 = fvk1.payment_address(1.into()).0;
192        let addr2_0 = fvk2.payment_address(0.into()).0;
193        let addr2_1 = fvk2.payment_address(1.into()).0;
194
195        assert_eq!(
196            fvk1.view_address(addr1_0.clone()),
197            AddressView::Decoded {
198                address: addr1_0.clone(),
199                index: 0.into(),
200                wallet_id: fvk1.wallet_id(),
201            }
202        );
203        assert_eq!(
204            fvk2.view_address(addr1_0.clone()),
205            AddressView::Opaque {
206                address: addr1_0.clone()
207            }
208        );
209        assert_eq!(
210            fvk1.view_address(addr1_1.clone()),
211            AddressView::Decoded {
212                address: addr1_1.clone(),
213                index: 1.into(),
214                wallet_id: fvk1.wallet_id(),
215            }
216        );
217        assert_eq!(
218            fvk2.view_address(addr1_1.clone()),
219            AddressView::Opaque {
220                address: addr1_1.clone()
221            }
222        );
223        assert_eq!(
224            fvk1.view_address(addr2_0.clone()),
225            AddressView::Opaque {
226                address: addr2_0.clone()
227            }
228        );
229        assert_eq!(
230            fvk2.view_address(addr2_0.clone()),
231            AddressView::Decoded {
232                address: addr2_0.clone(),
233                index: 0.into(),
234                wallet_id: fvk2.wallet_id(),
235            }
236        );
237        assert_eq!(
238            fvk1.view_address(addr2_1.clone()),
239            AddressView::Opaque {
240                address: addr2_1.clone()
241            }
242        );
243        assert_eq!(
244            fvk2.view_address(addr2_1.clone()),
245            AddressView::Decoded {
246                address: addr2_1.clone(),
247                index: 1.into(),
248                wallet_id: fvk2.wallet_id(),
249            }
250        );
251    }
252}