2018-12-05 21:37:11 +03:00
|
|
|
// Copyright 2018 The Grin Developers
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
// Keybase Wallet Plugin
|
|
|
|
|
2018-12-08 02:59:40 +03:00
|
|
|
use crate::controller;
|
2019-02-06 02:02:00 +03:00
|
|
|
use crate::libwallet::slate::Slate;
|
2018-12-08 02:59:40 +03:00
|
|
|
use crate::libwallet::{Error, ErrorKind};
|
|
|
|
use crate::{instantiate_wallet, HTTPNodeClient, WalletCommAdapter, WalletConfig};
|
2018-12-05 21:37:11 +03:00
|
|
|
use failure::ResultExt;
|
|
|
|
use serde::Serialize;
|
2018-12-08 02:59:40 +03:00
|
|
|
use serde_json::{from_str, json, to_string, Value};
|
2018-12-05 21:37:11 +03:00
|
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
use std::process::{Command, Stdio};
|
|
|
|
use std::str::from_utf8;
|
|
|
|
use std::thread::sleep;
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
|
|
|
|
const TTL: u16 = 60; // TODO: Pass this as a parameter
|
2018-12-25 03:05:24 +03:00
|
|
|
const LISTEN_SLEEP_DURATION: Duration = Duration::from_millis(5000);
|
|
|
|
const POLL_SLEEP_DURATION: Duration = Duration::from_millis(1000);
|
2018-12-05 21:37:11 +03:00
|
|
|
|
|
|
|
// Which topic names to use for communication
|
|
|
|
const SLATE_NEW: &str = "grin_slate_new";
|
|
|
|
const SLATE_SIGNED: &str = "grin_slate_signed";
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct KeybaseWalletCommAdapter {}
|
|
|
|
|
|
|
|
impl KeybaseWalletCommAdapter {
|
|
|
|
/// Check if keybase is installed and return an adapter object.
|
|
|
|
pub fn new() -> Box<WalletCommAdapter> {
|
|
|
|
let mut proc = if cfg!(target_os = "windows") {
|
|
|
|
Command::new("where")
|
|
|
|
} else {
|
|
|
|
Command::new("which")
|
|
|
|
};
|
|
|
|
proc.arg("keybase")
|
|
|
|
.stdout(Stdio::null())
|
|
|
|
.status()
|
|
|
|
.expect("Keybase executable not found, make sure it is installed and in your PATH");
|
|
|
|
|
|
|
|
Box::new(KeybaseWalletCommAdapter {})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Send a json object to the keybase process. Type `keybase chat api --help` for a list of available methods.
|
2018-12-25 03:05:24 +03:00
|
|
|
fn api_send(payload: &str) -> Result<Value, Error> {
|
2018-12-05 21:37:11 +03:00
|
|
|
let mut proc = Command::new("keybase");
|
|
|
|
proc.args(&["chat", "api", "-m", &payload]);
|
2018-12-25 03:05:24 +03:00
|
|
|
let output = proc.output().expect("No output");
|
|
|
|
if !output.status.success() {
|
|
|
|
error!(
|
|
|
|
"keybase api fail: {} {}",
|
|
|
|
String::from_utf8_lossy(&output.stdout),
|
|
|
|
String::from_utf8_lossy(&output.stderr)
|
|
|
|
);
|
|
|
|
Err(ErrorKind::GenericError("keybase api fail".to_owned()))?
|
|
|
|
} else {
|
|
|
|
let response: Value =
|
|
|
|
from_str(from_utf8(&output.stdout).expect("Bad output")).expect("Bad output");
|
|
|
|
let err_msg = format!("{}", response["error"]["message"]);
|
|
|
|
if err_msg.len() > 0 && err_msg != "null" {
|
|
|
|
error!("api_send got error: {}", err_msg);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(response)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get keybase username
|
|
|
|
fn whoami() -> Result<String, Error> {
|
|
|
|
let mut proc = Command::new("keybase");
|
|
|
|
proc.args(&["status", "-json"]);
|
|
|
|
let output = proc.output().expect("No output");
|
|
|
|
if !output.status.success() {
|
|
|
|
error!(
|
|
|
|
"keybase api fail: {} {}",
|
|
|
|
String::from_utf8_lossy(&output.stdout),
|
|
|
|
String::from_utf8_lossy(&output.stderr)
|
|
|
|
);
|
|
|
|
Err(ErrorKind::GenericError("keybase api fail".to_owned()))?
|
|
|
|
} else {
|
|
|
|
let response: Value =
|
|
|
|
from_str(from_utf8(&output.stdout).expect("Bad output")).expect("Bad output");
|
|
|
|
let err_msg = format!("{}", response["error"]["message"]);
|
|
|
|
if err_msg.len() > 0 && err_msg != "null" {
|
|
|
|
error!("status query got error: {}", err_msg);
|
|
|
|
}
|
|
|
|
|
|
|
|
let username = response["Username"].as_str();
|
|
|
|
if let Some(s) = username {
|
|
|
|
Ok(s.to_string())
|
|
|
|
} else {
|
|
|
|
error!("keybase username query fail");
|
|
|
|
Err(ErrorKind::GenericError(
|
|
|
|
"keybase username query fail".to_owned(),
|
|
|
|
))?
|
|
|
|
}
|
|
|
|
}
|
2018-12-05 21:37:11 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Get all unread messages from a specific channel/topic and mark as read.
|
2018-12-25 03:05:24 +03:00
|
|
|
fn read_from_channel(channel: &str, topic: &str) -> Result<Vec<String>, Error> {
|
2018-12-05 21:37:11 +03:00
|
|
|
let payload = to_string(&json!({
|
2018-12-08 02:59:40 +03:00
|
|
|
"method": "read",
|
|
|
|
"params": {
|
|
|
|
"options": {
|
|
|
|
"channel": {
|
|
|
|
"name": channel, "topic_type": "dev", "topic_name": topic
|
|
|
|
},
|
2018-12-05 21:37:11 +03:00
|
|
|
"unread_only": true, "peek": false
|
2018-12-08 02:59:40 +03:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
))
|
|
|
|
.unwrap();
|
2018-12-05 21:37:11 +03:00
|
|
|
|
|
|
|
let response = api_send(&payload);
|
2018-12-25 03:05:24 +03:00
|
|
|
if let Ok(res) = response {
|
|
|
|
let mut unread: Vec<String> = Vec::new();
|
|
|
|
for msg in res["result"]["messages"]
|
|
|
|
.as_array()
|
|
|
|
.unwrap_or(&vec![json!({})])
|
|
|
|
.iter()
|
|
|
|
{
|
|
|
|
if (msg["msg"]["content"]["type"] == "text") && (msg["msg"]["unread"] == true) {
|
|
|
|
let message = msg["msg"]["content"]["text"]["body"].as_str().unwrap_or("");
|
|
|
|
unread.push(message.to_owned());
|
|
|
|
}
|
2018-12-05 21:37:11 +03:00
|
|
|
}
|
2018-12-25 03:05:24 +03:00
|
|
|
Ok(unread)
|
|
|
|
} else {
|
|
|
|
Err(ErrorKind::GenericError("keybase api fail".to_owned()))?
|
2018-12-05 21:37:11 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get unread messages from all channels and mark as read.
|
2018-12-25 03:05:24 +03:00
|
|
|
fn get_unread(topic: &str) -> Result<HashMap<String, String>, Error> {
|
2018-12-05 21:37:11 +03:00
|
|
|
let payload = to_string(&json!({
|
2018-12-08 02:59:40 +03:00
|
|
|
"method": "list",
|
|
|
|
"params": {
|
|
|
|
"options": {
|
2018-12-25 03:05:24 +03:00
|
|
|
"topic_type": "dev",
|
|
|
|
},
|
2018-12-08 02:59:40 +03:00
|
|
|
}
|
2018-12-25 03:05:24 +03:00
|
|
|
}))
|
2018-12-08 02:59:40 +03:00
|
|
|
.unwrap();
|
2018-12-05 21:37:11 +03:00
|
|
|
let response = api_send(&payload);
|
|
|
|
|
2018-12-25 03:05:24 +03:00
|
|
|
if let Ok(res) = response {
|
|
|
|
let mut channels = HashSet::new();
|
|
|
|
// Unfortunately the response does not contain the message body
|
|
|
|
// and a separate call is needed for each channel
|
|
|
|
for msg in res["result"]["conversations"]
|
|
|
|
.as_array()
|
|
|
|
.unwrap_or(&vec![json!({})])
|
|
|
|
.iter()
|
|
|
|
{
|
|
|
|
if (msg["unread"] == true) && (msg["channel"]["topic_name"] == topic) {
|
|
|
|
let channel = msg["channel"]["name"].as_str().unwrap();
|
|
|
|
channels.insert(channel.to_string());
|
|
|
|
}
|
2018-12-05 21:37:11 +03:00
|
|
|
}
|
2018-12-25 03:05:24 +03:00
|
|
|
let mut unread: HashMap<String, String> = HashMap::new();
|
|
|
|
for channel in channels.iter() {
|
|
|
|
let messages = read_from_channel(channel, topic);
|
|
|
|
if messages.is_err() {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
for msg in messages.unwrap() {
|
|
|
|
unread.insert(msg, channel.to_string());
|
|
|
|
}
|
2018-12-05 21:37:11 +03:00
|
|
|
}
|
2018-12-25 03:05:24 +03:00
|
|
|
Ok(unread)
|
|
|
|
} else {
|
|
|
|
Err(ErrorKind::GenericError("keybase api fail".to_owned()))?
|
2018-12-05 21:37:11 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Send a message to a keybase channel that self-destructs after ttl seconds.
|
|
|
|
fn send<T: Serialize>(message: T, channel: &str, topic: &str, ttl: u16) -> bool {
|
|
|
|
let seconds = format!("{}s", ttl);
|
|
|
|
let payload = to_string(&json!({
|
2018-12-05 22:05:13 +03:00
|
|
|
"method": "send",
|
2018-12-05 21:37:11 +03:00
|
|
|
"params": {
|
|
|
|
"options": {
|
|
|
|
"channel": {
|
2018-12-25 03:05:24 +03:00
|
|
|
"name": channel, "topic_name": topic, "topic_type": "dev"
|
|
|
|
},
|
|
|
|
"message": {
|
|
|
|
"body": to_string(&message).unwrap()
|
|
|
|
},
|
|
|
|
"exploding_lifetime": seconds
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
.unwrap();
|
|
|
|
let response = api_send(&payload);
|
|
|
|
if let Ok(res) = response {
|
|
|
|
match res["result"]["message"].as_str() {
|
|
|
|
Some("message sent") => true,
|
|
|
|
_ => false,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Send a notify to self that self-destructs after ttl minutes.
|
|
|
|
fn notify(message: &str, channel: &str, ttl: u16) -> bool {
|
|
|
|
let minutes = format!("{}m", ttl);
|
|
|
|
let payload = to_string(&json!({
|
|
|
|
"method": "send",
|
|
|
|
"params": {
|
|
|
|
"options": {
|
|
|
|
"channel": {
|
|
|
|
"name": channel
|
|
|
|
},
|
|
|
|
"message": {
|
|
|
|
"body": message
|
|
|
|
},
|
|
|
|
"exploding_lifetime": minutes
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}))
|
2018-12-08 02:59:40 +03:00
|
|
|
.unwrap();
|
2018-12-05 21:37:11 +03:00
|
|
|
let response = api_send(&payload);
|
2018-12-25 03:05:24 +03:00
|
|
|
if let Ok(res) = response {
|
|
|
|
match res["result"]["message"].as_str() {
|
|
|
|
Some("message sent") => true,
|
|
|
|
_ => false,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
false
|
2018-12-05 21:37:11 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Listen for a message from a specific channel with topic SLATE_SIGNED for nseconds and return the first valid slate.
|
|
|
|
fn poll(nseconds: u64, channel: &str) -> Option<Slate> {
|
|
|
|
let start = Instant::now();
|
2018-12-25 03:05:24 +03:00
|
|
|
info!("Waiting for response message from @{}...", channel);
|
2018-12-05 21:37:11 +03:00
|
|
|
while start.elapsed().as_secs() < nseconds {
|
|
|
|
let unread = read_from_channel(channel, SLATE_SIGNED);
|
2018-12-25 03:05:24 +03:00
|
|
|
for msg in unread.unwrap().iter() {
|
2018-12-05 21:37:11 +03:00
|
|
|
let blob = from_str::<Slate>(msg);
|
|
|
|
match blob {
|
|
|
|
Ok(slate) => {
|
2018-12-25 03:05:24 +03:00
|
|
|
info!(
|
|
|
|
"keybase response message received from @{}, tx uuid: {}",
|
|
|
|
channel, slate.id,
|
|
|
|
);
|
2018-12-05 21:37:11 +03:00
|
|
|
return Some(slate);
|
|
|
|
}
|
|
|
|
Err(_) => (),
|
|
|
|
}
|
|
|
|
}
|
2018-12-25 03:05:24 +03:00
|
|
|
sleep(POLL_SLEEP_DURATION);
|
2018-12-05 21:37:11 +03:00
|
|
|
}
|
2018-12-25 03:05:24 +03:00
|
|
|
error!(
|
|
|
|
"No response from @{} in {} seconds. Grin send failed!",
|
2018-12-05 21:37:11 +03:00
|
|
|
channel, nseconds
|
|
|
|
);
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
|
|
|
impl WalletCommAdapter for KeybaseWalletCommAdapter {
|
|
|
|
fn supports_sync(&self) -> bool {
|
|
|
|
true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send a slate to a keybase username then wait for a response for TTL seconds.
|
|
|
|
fn send_tx_sync(&self, addr: &str, slate: &Slate) -> Result<Slate, Error> {
|
2018-12-25 03:05:24 +03:00
|
|
|
// Limit only one recipient
|
|
|
|
if addr.matches(",").count() > 0 {
|
|
|
|
error!("Only one recipient is supported!");
|
|
|
|
return Err(ErrorKind::GenericError("Tx rejected".to_owned()))?;
|
|
|
|
}
|
|
|
|
|
2018-12-05 21:37:11 +03:00
|
|
|
// Send original slate to recipient with the SLATE_NEW topic
|
|
|
|
match send(slate, addr, SLATE_NEW, TTL) {
|
|
|
|
true => (),
|
2019-01-25 15:12:50 +03:00
|
|
|
false => {
|
|
|
|
return Err(ErrorKind::ClientCallback(
|
|
|
|
"Posting transaction slate".to_owned(),
|
|
|
|
))?
|
|
|
|
}
|
2018-12-05 21:37:11 +03:00
|
|
|
}
|
2018-12-25 03:05:24 +03:00
|
|
|
info!(
|
|
|
|
"tx request has been sent to @{}, tx uuid: {}",
|
|
|
|
addr, slate.id
|
|
|
|
);
|
2018-12-05 21:37:11 +03:00
|
|
|
// Wait for response from recipient with SLATE_SIGNED topic
|
|
|
|
match poll(TTL as u64, addr) {
|
|
|
|
Some(slate) => return Ok(slate),
|
2019-01-25 15:12:50 +03:00
|
|
|
None => {
|
|
|
|
return Err(ErrorKind::ClientCallback(
|
|
|
|
"Receiving reply from recipient".to_owned(),
|
|
|
|
))?
|
|
|
|
}
|
2018-12-05 21:37:11 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Send a transaction asynchronously (result will be returned via the listener)
|
|
|
|
fn send_tx_async(&self, _addr: &str, _slate: &Slate) -> Result<(), Error> {
|
|
|
|
unimplemented!();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Receive a transaction async. (Actually just read it from wherever and return the slate)
|
|
|
|
fn receive_tx_async(&self, _params: &str) -> Result<Slate, Error> {
|
|
|
|
unimplemented!();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Start a listener, passing received messages to the wallet api directly
|
|
|
|
#[allow(unreachable_code)]
|
|
|
|
fn listen(
|
|
|
|
&self,
|
|
|
|
_params: HashMap<String, String>,
|
|
|
|
config: WalletConfig,
|
|
|
|
passphrase: &str,
|
|
|
|
account: &str,
|
|
|
|
node_api_secret: Option<String>,
|
|
|
|
) -> Result<(), Error> {
|
2018-12-07 18:06:28 +03:00
|
|
|
let node_client = HTTPNodeClient::new(&config.check_node_api_http_addr, node_api_secret);
|
|
|
|
let wallet = instantiate_wallet(config.clone(), node_client, passphrase, account)
|
|
|
|
.context(ErrorKind::WalletSeedDecryption)?;
|
2018-12-05 21:37:11 +03:00
|
|
|
|
2018-12-25 03:05:24 +03:00
|
|
|
info!("Listening for transactions on keybase ...");
|
2018-12-05 21:37:11 +03:00
|
|
|
loop {
|
|
|
|
// listen for messages from all channels with topic SLATE_NEW
|
|
|
|
let unread = get_unread(SLATE_NEW);
|
2018-12-25 03:05:24 +03:00
|
|
|
if unread.is_err() {
|
|
|
|
error!("Listening exited for some keybase api failure");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
for (msg, channel) in &unread.unwrap() {
|
2018-12-05 21:37:11 +03:00
|
|
|
let blob = from_str::<Slate>(msg);
|
|
|
|
match blob {
|
|
|
|
Ok(mut slate) => {
|
2018-12-25 03:05:24 +03:00
|
|
|
let tx_uuid = slate.id;
|
|
|
|
|
|
|
|
// Reject multiple recipients channel for safety
|
|
|
|
{
|
2018-12-25 07:09:43 +03:00
|
|
|
if channel.matches(",").count() > 1 {
|
2018-12-25 03:05:24 +03:00
|
|
|
error!(
|
|
|
|
"Incoming tx initiated on channel \"{}\" is rejected, multiple recipients channel! amount: {}(g), tx uuid: {}",
|
|
|
|
channel,
|
|
|
|
slate.amount as f64 / 1000000000.0,
|
|
|
|
tx_uuid,
|
|
|
|
);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
info!(
|
|
|
|
"tx initiated on channel \"{}\", to send you {}(g). tx uuid: {}",
|
|
|
|
channel,
|
|
|
|
slate.amount as f64 / 1000000000.0,
|
|
|
|
tx_uuid,
|
|
|
|
);
|
2018-12-05 21:37:11 +03:00
|
|
|
match controller::foreign_single_use(wallet.clone(), |api| {
|
2018-12-23 02:25:02 +03:00
|
|
|
if let Err(e) = api.verify_slate_messages(&slate) {
|
|
|
|
error!("Error validating participant messages: {}", e);
|
|
|
|
return Err(e);
|
|
|
|
}
|
2018-12-05 21:37:11 +03:00
|
|
|
api.receive_tx(&mut slate, None, None)?;
|
|
|
|
Ok(())
|
|
|
|
}) {
|
|
|
|
// Reply to the same channel with topic SLATE_SIGNED
|
|
|
|
Ok(_) => match send(slate, channel, SLATE_SIGNED, TTL) {
|
|
|
|
true => {
|
2018-12-25 03:05:24 +03:00
|
|
|
notify_on_receive(
|
2018-12-25 07:09:43 +03:00
|
|
|
config.keybase_notify_ttl.unwrap_or(1440),
|
2018-12-25 03:05:24 +03:00
|
|
|
channel.to_string(),
|
|
|
|
tx_uuid.to_string(),
|
|
|
|
);
|
|
|
|
debug!("Returned slate to @{} via keybase", channel);
|
2018-12-05 21:37:11 +03:00
|
|
|
}
|
|
|
|
false => {
|
2018-12-25 03:05:24 +03:00
|
|
|
error!("Failed to return slate to @{} via keybase. Incoming tx failed", channel);
|
2018-12-05 21:37:11 +03:00
|
|
|
}
|
|
|
|
},
|
|
|
|
Err(e) => {
|
2018-12-25 03:05:24 +03:00
|
|
|
error!(
|
|
|
|
"Error on receiving tx via keybase: {}. Incoming tx failed",
|
|
|
|
e
|
|
|
|
);
|
2018-12-05 21:37:11 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(_) => (),
|
|
|
|
}
|
|
|
|
}
|
2018-12-25 03:05:24 +03:00
|
|
|
sleep(LISTEN_SLEEP_DURATION);
|
2018-12-05 21:37:11 +03:00
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
2018-12-25 03:05:24 +03:00
|
|
|
|
|
|
|
/// Notify in keybase on receiving a transaction
|
|
|
|
fn notify_on_receive(keybase_notify_ttl: u16, channel: String, tx_uuid: String) {
|
|
|
|
if keybase_notify_ttl > 0 {
|
|
|
|
let my_username = whoami();
|
|
|
|
if let Ok(username) = my_username {
|
|
|
|
let split = channel.split(",");
|
|
|
|
let vec: Vec<&str> = split.collect();
|
|
|
|
if vec.len() > 1 {
|
|
|
|
let receiver = username;
|
|
|
|
let sender = if vec[0] == receiver {
|
|
|
|
vec[1]
|
|
|
|
} else {
|
|
|
|
if vec[1] != receiver {
|
|
|
|
error!("keybase - channel doesn't include my username! channel: {}, username: {}",
|
|
|
|
channel, receiver
|
|
|
|
);
|
|
|
|
}
|
|
|
|
vec[0]
|
|
|
|
};
|
|
|
|
|
|
|
|
let msg = format!(
|
|
|
|
"[grin wallet notice]: \
|
|
|
|
you could have some coins received from @{}\n\
|
|
|
|
Transaction Id: {}",
|
|
|
|
sender, tx_uuid
|
|
|
|
);
|
|
|
|
notify(&msg, &receiver, keybase_notify_ttl);
|
|
|
|
info!(
|
|
|
|
"tx from @{} is done, please check on grin wallet. tx uuid: {}",
|
|
|
|
sender, tx_uuid,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
error!("keybase notification fail on whoami query");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|