tape-sdk 0.4.4

High-level SDK for tapedrive blob upload/download operations
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
//! High-level client for the Tapedrive storage network.

use std::sync::Arc;

use arc_swap::ArcSwap;
use tape_peer_http::{HttpApi, HttpApiBuilder};
use tape_peer_manager::PeerManager;
use tape_rpc::Rpc;
use tape_rpc_client::RpcClient;
use tape_api::program::tapedrive;
use tape_core::prelude::{CompressedTrack, StorageUnits};
use tape_core::types::coin::{SOL, TAPE};
use tape_core::types::ContentType;
use tape_crypto::prelude::{Address, Keypair};
use tape_protocol::{Api, ProtocolState};
use tokio::io::{AsyncRead, AsyncWrite};

use crate::balance::{sol_balance_of, tape_balance_of};
use crate::bootstrap::{BootstrapStore, Reputation};
use crate::error::TapedriveError;
use crate::keys::operator::TapeOperator;
use crate::keys::tape_key::TapeKey;
use crate::metrics::{Metrics, Noop, Operation, Phase, Timer};
use crate::read_options::ReadOptions;
use crate::write_options::WriteOptions;
use crate::stream::{
    read::{read_bytes, read_into},
    receipt::StreamReceipt,
    write::{write_bytes as write_stream_bytes, write_stream as write_reader_stream},
};
use crate::track::write::{write_or_resume, UNNAMED_TRACK, UNTYPED_TRACK};

/// High-level client for the Tapedrive storage network.
///
/// Generic over `Blockchain: Rpc` (on-chain) and `Cluster: Api` (storage nodes).
pub struct Tapedrive<Blockchain: Rpc, Cluster: Api> {
    pub state: ArcSwap<ProtocolState>,
    pub peer_manager: Arc<PeerManager>,
    pub api: Arc<Cluster>,
    pub rpc: Arc<RpcClient<Blockchain>>,
    pub payer: Option<Keypair>,
    pub metrics: Arc<dyn Metrics>,
    pub write_options: WriteOptions,
    pub read_options: ReadOptions,
    pub reputation: Arc<Reputation>,
}

/// Default constructor using `HttpApi`.
impl<Blockchain: Rpc> Tapedrive<Blockchain, HttpApi> {
    /// Create a new Tapedrive client.
    ///
    /// Takes an RPC backend and a payer keypair. Uses the default HTTP
    /// peer client for storage node communication.
    pub fn new(rpc: Blockchain, payer: Keypair) -> Self {
        Self::new_read_only(rpc).with_payer(payer)
    }

    /// Create a read-only client that presents a reader identity over mTLS.
    ///
    /// Nodes match a hidden track's share token against this identity's pubkey.
    pub fn new_with_identity(
        rpc: Blockchain,
        identity: Arc<Keypair>,
    ) -> Result<Self, tape_peer_tls::TlsError> {
        let rpc_client = Arc::new(RpcClient::from_rpc(rpc));
        let peer_manager = Arc::new(PeerManager::new());
        let api = Arc::new(
            HttpApiBuilder::new()
                .local_identity(identity)
                .build(peer_manager.clone())?,
        );

        Ok(Self::from_parts(
            ArcSwap::from_pointee(ProtocolState::default()),
            peer_manager,
            api,
            rpc_client,
            None,
        ))
    }

    /// Create a read-only Tapedrive client.
    pub fn new_read_only(rpc: Blockchain) -> Self {
        let rpc_client = Arc::new(RpcClient::from_rpc(rpc));
        let peer_manager = Arc::new(PeerManager::new());
        let api = Arc::new(HttpApi::with_default_timeouts(peer_manager.clone()));
        Self::from_parts(
            ArcSwap::from_pointee(ProtocolState::default()),
            peer_manager,
            api,
            rpc_client,
            None,
        )
    }
}

impl<Blockchain: Rpc, Cluster: Api> Tapedrive<Blockchain, Cluster> {
    /// Create a Tapedrive client from existing parts.
    pub fn from_parts(
        state: ArcSwap<ProtocolState>,
        peer_manager: Arc<PeerManager>,
        api: Arc<Cluster>,
        rpc: Arc<RpcClient<Blockchain>>,
        payer: Option<Keypair>,
    ) -> Self {
        Self {
            state,
            peer_manager,
            api,
            rpc,
            payer,
            metrics: Arc::new(Noop),
            write_options: WriteOptions::default(),
            read_options: ReadOptions::default(),
            reputation: Arc::new(Reputation::detached()),
        }
    }

    /// Replace the cached network state, freshly verified.
    ///
    /// The mark goes on before the store, so the state is never visible in the
    /// distrusted window between the two.
    pub fn store_state(&self, state: ProtocolState) {
        state.touch();
        self.state.store(Arc::new(state));
    }

    /// Share an existing reputation table rather than starting a fresh one.
    ///
    /// A long-lived caller that rebuilds a client per request must pass its own
    /// here, otherwise every request starts with an empty table and the peer
    /// ordering never learns anything.
    pub fn with_reputation(mut self, reputation: Arc<Reputation>) -> Self {
        self.reputation = reputation;
        self
    }

    /// Persist peer reputation and bootstrap hints to this store.
    pub fn with_bootstrap_cache(mut self, store: BootstrapStore) -> Self {
        self.reputation = Arc::new(Reputation::attach(store, tapedrive::id().into()));
        self
    }

    /// Attach or replace the payer used for mutating operations.
    pub fn with_payer(mut self, payer: Keypair) -> Self {
        self.payer = Some(payer);
        self
    }

    /// Replace the write concurrency knobs.
    pub fn with_write_options(mut self, options: WriteOptions) -> Self {
        self.write_options = options;
        self
    }

    /// Replace the read concurrency knobs.
    pub fn with_read_options(mut self, options: ReadOptions) -> Self {
        self.read_options = options;
        self
    }

    /// Attach or replace the metrics recorder.
    pub fn with_metrics(mut self, metrics: Arc<dyn Metrics>) -> Self {
        self.metrics = metrics;
        self
    }

    /// Access the underlying RPC client.
    pub fn rpc(&self) -> &RpcClient<Blockchain> {
        &self.rpc
    }

    /// Load the current protocol state (lock-free).
    pub fn state(&self) -> arc_swap::Guard<Arc<ProtocolState>> {
        self.state.load()
    }

    /// Return the payer keypair required for mutating operations.
    pub fn payer(&self) -> Result<&Keypair, TapedriveError> {
        self.payer.as_ref().ok_or(TapedriveError::MissingPayer)
    }

    /// The payer's SOL balance in lamports. A missing account reads as zero.
    pub async fn sol_balance(&self) -> Result<SOL, TapedriveError> {
        sol_balance_of(&self.rpc, &self.payer()?.address()).await
    }

    /// The payer's TAPE balance in flux. A missing token account reads as zero.
    pub async fn tape_balance(&self) -> Result<TAPE, TapedriveError> {
        tape_balance_of(&self.rpc, &self.payer()?.address()).await
    }

    pub(crate) fn timer(&self, operation: Operation, phase: Phase) -> Timer<'_> {
        Timer::start(self.metrics.as_ref(), operation, phase)
    }

    /// Write unnamed content-addressed data to the network in one call.
    ///
    /// Reserves the tape controlled by `tape_key` sized to fit `data`, registers
    /// a track, uploads erasure-coded slices to storage nodes, and certifies the
    /// track with BLS signatures. Unnamed tracks are excluded from object
    /// listings.
    ///
    /// The caller owns the tape key: generate and durably persist it before
    /// calling, so an interrupted write leaves the reserved tape recoverable.
    pub async fn write(
        &self,
        tape_key: &TapeKey,
        data: &[u8],
        epochs: u64,
    ) -> Result<CompressedTrack, TapedriveError> {
        self.write_named(
            tape_key,
            UNNAMED_TRACK,
            UNTYPED_TRACK,
            data,
            epochs,
        )
        .await
    }

    /// Write named data to the network in one call.
    ///
    /// Named tracks on non-system tapes are materialized into object listings.
    /// The caller owns the tape key, same as write.
    pub async fn write_named(
        &self,
        tape_key: &TapeKey,
        name: impl AsRef<[u8]>,
        content_type: ContentType,
        data: &[u8],
        epochs: u64,
    ) -> Result<CompressedTrack, TapedriveError> {
        let total = self
            .timer(Operation::Write, Phase::Total)
            .bytes(data.len() as u64);

        let result =
            write_or_resume(
                self,
                tape_key,
                name.as_ref(),
                content_type,
                data,
                epochs,
                self.write_options.visibility,
            )
            .await;
        total.finish_result(&result);

        result
    }

    /// Write unnamed in-memory bytes to an existing tape as a logical stream.
    ///
    /// Always writes a manifest track as the last track. For streams that fit
    /// in a single chunk, one data track and one manifest track are written.
    /// The manifest and chunks are excluded from object listings.
    pub async fn write_bytes(
        &self,
        tape_key: &TapeKey,
        data: &[u8],
    ) -> Result<StreamReceipt, TapedriveError> {
        self.write_named_bytes(
            tape_key,
            UNNAMED_TRACK,
            UNTYPED_TRACK,
            data,
        )
        .await
    }

    /// Write named in-memory bytes to an existing tape as a logical stream.
    ///
    /// The manifest track carries the object's name and content type; internal
    /// chunk tracks remain unnamed.
    pub async fn write_named_bytes(
        &self,
        tape_key: &TapeKey,
        name: impl AsRef<[u8]>,
        content_type: ContentType,
        data: &[u8],
    ) -> Result<StreamReceipt, TapedriveError> {
        self.write_named_bytes_as(tape_key, name, content_type, data)
            .await
    }

    /// Write named in-memory bytes as a stream.
    pub async fn write_named_bytes_as(
        &self,
        operator: &impl TapeOperator,
        name: impl AsRef<[u8]>,
        content_type: ContentType,
        data: &[u8],
    ) -> Result<StreamReceipt, TapedriveError> {
        let timer = self
            .timer(Operation::WriteStream, Phase::Total)
            .bytes(data.len() as u64);
        let result = write_stream_bytes(self, operator, name.as_ref(), content_type, data).await;
        timer.finish_result(&result);
        result
    }

    /// Write an unnamed byte stream from an async reader into an existing tape.
    ///
    /// The reader must yield exactly `size` bytes. The manifest and chunks are
    /// excluded from object listings.
    pub async fn write_stream<Reader: AsyncRead + Unpin>(
        &self,
        tape_key: &TapeKey,
        size: StorageUnits,
        reader: Reader,
    ) -> Result<StreamReceipt, TapedriveError> {
        self.write_named_stream(
            tape_key,
            UNNAMED_TRACK,
            UNTYPED_TRACK,
            size,
            reader,
        )
        .await
    }

    /// Write a named byte stream from an async reader into an existing tape.
    ///
    /// The manifest track carries the object's name and content type; internal
    /// chunk tracks remain unnamed.
    pub async fn write_named_stream<Reader: AsyncRead + Unpin>(
        &self,
        tape_key: &TapeKey,
        name: impl AsRef<[u8]>,
        content_type: ContentType,
        size: StorageUnits,
        reader: Reader,
    ) -> Result<StreamReceipt, TapedriveError> {
        self.write_named_stream_as(tape_key, name, content_type, size, reader)
            .await
    }

    /// Write a named byte stream from an async reader.
    pub async fn write_named_stream_as<Reader: AsyncRead + Unpin>(
        &self,
        operator: &impl TapeOperator,
        name: impl AsRef<[u8]>,
        content_type: ContentType,
        size: StorageUnits,
        reader: Reader,
    ) -> Result<StreamReceipt, TapedriveError> {
        let timer = self
            .timer(Operation::WriteStream, Phase::Total)
            .bytes(size.to_bytes());
        let result =
            write_reader_stream(self, operator, name.as_ref(), content_type, size, reader).await;
        timer.finish_result(&result);
        result
    }

    /// Read a stored stream by its manifest track address into memory.
    pub async fn read_bytes(
        &self,
        manifest: &Address,
    ) -> Result<Vec<u8>, TapedriveError> {
        let timer = self.timer(Operation::ReadStream, Phase::Total);
        let result = read_bytes(self, manifest).await;
        let timer = match &result {
            Ok(bytes) => timer.bytes(bytes.len() as u64),
            Err(_) => timer,
        };
        timer.finish_result(&result);
        result
    }

    /// Read a stored stream by its manifest track address into an async sink.
    pub async fn read_into<Writer: AsyncWrite + Unpin>(
        &self,
        manifest: &Address,
        writer: Writer,
    ) -> Result<(), TapedriveError> {
        let timer = self.timer(Operation::ReadStream, Phase::Total);
        let result = read_into(self, manifest, writer).await;
        timer.finish_result(&result);
        result
    }
}

#[cfg(test)]
mod tests {
    use rpc_litesvm::LiteSvmRpc;
    use tape_crypto::prelude::Keypair;

    use super::*;

    // fresh accounts read zero and an airdrop shows up in the sol balance
    #[tokio::test]
    async fn balances() {
        let rpc = LiteSvmRpc::new();
        let payer = Keypair::new(&mut rand::thread_rng());
        let address = payer.address();
        let client = Tapedrive::new(rpc.clone(), payer);

        assert_eq!(client.sol_balance().await.expect("sol balance"), SOL(0));
        assert_eq!(client.tape_balance().await.expect("tape balance"), TAPE(0));

        rpc.airdrop(&address.into(), 5_000_000_000).expect("airdrop");
        assert_eq!(
            client.sol_balance().await.expect("sol balance"),
            SOL(5_000_000_000)
        );
    }
}