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
use super::{Error, SpspResponse};
use futures::{future::result, Future};
use interledger_packet::Address;
use interledger_service::{Account, IncomingService};
use interledger_stream::send_money;
use log::{debug, error, trace};
use reqwest::r#async::Client;
use std::convert::TryFrom;

pub fn query(server: &str) -> impl Future<Item = SpspResponse, Error = Error> {
    let server = payment_pointer_to_url(server);
    trace!("Querying receiver: {}", server);

    let client = Client::new();
    client
        .get(&server)
        .header("Accept", "application/spsp4+json")
        .send()
        .map_err(|err| Error::HttpError(format!("Error querying SPSP receiver: {:?}", err)))
        .and_then(|mut res| {
            res.json::<SpspResponse>()
                .map_err(|err| Error::InvalidResponseError(format!("{:?}", err)))
        })
}

/// Query the details of the given Payment Pointer and send a payment using the STREAM protocol.
///
/// This returns the amount delivered, as reported by the receiver and in the receiver's asset's units.
pub fn pay<S, A>(
    service: S,
    from_account: A,
    receiver: &str,
    source_amount: u64,
) -> impl Future<Item = u64, Error = Error>
where
    S: IncomingService<A> + Clone,
    A: Account,
{
    query(receiver).and_then(move |spsp| {
        let shared_secret = spsp.shared_secret;
        let dest = spsp.destination_account;
        result(Address::try_from(dest).map_err(move |err| {
            error!("Error parsing address");
            Error::InvalidResponseError(err.to_string())
        }))
        .and_then(move |addr| {
            debug!("Sending SPSP payment to address: {}", addr);

            send_money(service, &from_account, addr, &shared_secret, source_amount)
                .map(move |(amount_delivered, _plugin)| {
                    debug!(
                        "Sent SPSP payment of {} and delivered {} of the receiver's units",
                        source_amount, amount_delivered
                    );
                    amount_delivered
                })
                .map_err(move |err| {
                    error!("Error sending payment: {:?}", err);
                    Error::SendMoneyError(source_amount)
                })
        })
    })
}

fn payment_pointer_to_url(payment_pointer: &str) -> String {
    let mut url: String = if payment_pointer.starts_with('$') {
        let mut url = "https://".to_string();
        url.push_str(&payment_pointer[1..]);
        url
    } else {
        payment_pointer.to_string()
    };

    let num_slashes = url.matches('/').count();
    if num_slashes == 2 {
        url.push_str("/.well-known/pay");
    } else if num_slashes == 1 && url.ends_with('/') {
        url.push_str(".well-known/pay");
    }
    trace!(
        "Converted payment pointer: {} to URL: {}",
        payment_pointer,
        url
    );
    url
}

#[cfg(test)]
mod payment_pointer {
    use super::*;

    #[test]
    fn converts_pointer() {
        let pointer = "$subdomain.domain.example";
        assert_eq!(
            payment_pointer_to_url(pointer),
            "https://subdomain.domain.example/.well-known/pay"
        );
    }
}