mirror of
https://github.com/mimblewimble/grin.git
synced 2025-02-01 17:01:09 +03:00
Keybase wallet plugin (#2052)
* add keybase wallet plugin * rustfmt * cleanup * rustfmt * handle null case * Use two seperate topics for the two parts of the round trip * rustfmt * send slate to wallet directly (no need to run http listener anymore) * rustfmt * skip some unnecessary api calls * update docs
This commit is contained in:
parent
ed2e1f87aa
commit
f9261310ab
7 changed files with 309 additions and 6 deletions
|
@ -430,3 +430,25 @@ grin wallet restore
|
||||||
Note this operation can potentially take a long time. Once it's done, your wallet outputs should be restored, and you can
|
Note this operation can potentially take a long time. Once it's done, your wallet outputs should be restored, and you can
|
||||||
transact with your restored wallet as before the backup. Your transaction log history is not restored, and will simply
|
transact with your restored wallet as before the backup. Your transaction log history is not restored, and will simply
|
||||||
contain incoming transactions for each output found.
|
contain incoming transactions for each output found.
|
||||||
|
|
||||||
|
## Wallet plugins
|
||||||
|
|
||||||
|
Other than the default communication methods (http, file), grin exposes an interface that developers can use to integrate
|
||||||
|
any communication channel (i.e Telegram, Signal, email) for the exchange of slates.
|
||||||
|
|
||||||
|
### Keybase
|
||||||
|
|
||||||
|
Grin comes bundled with an experimental keybase.io plugin. The keybase client must be installed in the system. Usage is as follows:
|
||||||
|
|
||||||
|
Recipient starts a keybase listener.
|
||||||
|
```sh
|
||||||
|
grin wallet listen -m keybase
|
||||||
|
```
|
||||||
|
|
||||||
|
Sender creates a transaction, sends it to the recipient and awaits for the reply.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
grin wallet send <amount> -m keybase -d <recipient>
|
||||||
|
```
|
||||||
|
|
||||||
|
Where recipient is a keybase username. If everything goes well the transaction is finalized and sent to the node for broadcasting.
|
|
@ -29,8 +29,8 @@ use core::{core, global};
|
||||||
use grin_wallet::libwallet::ErrorKind;
|
use grin_wallet::libwallet::ErrorKind;
|
||||||
use grin_wallet::{self, controller, display, libwallet};
|
use grin_wallet::{self, controller, display, libwallet};
|
||||||
use grin_wallet::{
|
use grin_wallet::{
|
||||||
instantiate_wallet, FileWalletCommAdapter, HTTPNodeClient, HTTPWalletCommAdapter, LMDBBackend,
|
instantiate_wallet, FileWalletCommAdapter, HTTPNodeClient, HTTPWalletCommAdapter,
|
||||||
NullWalletCommAdapter, WalletConfig, WalletSeed,
|
KeybaseWalletCommAdapter, LMDBBackend, NullWalletCommAdapter, WalletConfig, WalletSeed,
|
||||||
};
|
};
|
||||||
use keychain;
|
use keychain;
|
||||||
use servers::start_webwallet_server;
|
use servers::start_webwallet_server;
|
||||||
|
@ -223,7 +223,13 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
|
||||||
params.insert("certificate".to_owned(), t.certificate);
|
params.insert("certificate".to_owned(), t.certificate);
|
||||||
params.insert("private_key".to_owned(), t.private_key);
|
params.insert("private_key".to_owned(), t.private_key);
|
||||||
}
|
}
|
||||||
let adapter = HTTPWalletCommAdapter::new();
|
let adapter = match listen_args.value_of("method") {
|
||||||
|
Some("http") => HTTPWalletCommAdapter::new(),
|
||||||
|
Some("keybase") => KeybaseWalletCommAdapter::new(),
|
||||||
|
_ => {
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
adapter
|
adapter
|
||||||
.listen(
|
.listen(
|
||||||
params,
|
params,
|
||||||
|
@ -452,6 +458,7 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
|
||||||
"http" => HTTPWalletCommAdapter::new(),
|
"http" => HTTPWalletCommAdapter::new(),
|
||||||
"file" => FileWalletCommAdapter::new(),
|
"file" => FileWalletCommAdapter::new(),
|
||||||
"self" => NullWalletCommAdapter::new(),
|
"self" => NullWalletCommAdapter::new(),
|
||||||
|
"keybase" => KeybaseWalletCommAdapter::new(),
|
||||||
_ => NullWalletCommAdapter::new(),
|
_ => NullWalletCommAdapter::new(),
|
||||||
};
|
};
|
||||||
if adapter.supports_sync() {
|
if adapter.supports_sync() {
|
||||||
|
|
|
@ -198,6 +198,13 @@ fn real_main() -> i32 {
|
||||||
.short("l")
|
.short("l")
|
||||||
.long("port")
|
.long("port")
|
||||||
.help("Port on which to run the wallet listener")
|
.help("Port on which to run the wallet listener")
|
||||||
|
.takes_value(true))
|
||||||
|
.arg(Arg::with_name("method")
|
||||||
|
.short("m")
|
||||||
|
.long("method")
|
||||||
|
.help("Which method to use for communication")
|
||||||
|
.possible_values(&["http", "keybase"])
|
||||||
|
.default_value("http")
|
||||||
.takes_value(true)))
|
.takes_value(true)))
|
||||||
|
|
||||||
.subcommand(SubCommand::with_name("owner_api")
|
.subcommand(SubCommand::with_name("owner_api")
|
||||||
|
@ -235,7 +242,7 @@ fn real_main() -> i32 {
|
||||||
.help("Method for sending this transaction.")
|
.help("Method for sending this transaction.")
|
||||||
.short("m")
|
.short("m")
|
||||||
.long("method")
|
.long("method")
|
||||||
.possible_values(&["http", "file", "self"])
|
.possible_values(&["http", "file", "self", "keybase"])
|
||||||
.default_value("http")
|
.default_value("http")
|
||||||
.takes_value(true))
|
.takes_value(true))
|
||||||
.arg(Arg::with_name("dest")
|
.arg(Arg::with_name("dest")
|
||||||
|
|
260
wallet/src/adapters/keybase.rs
Normal file
260
wallet/src/adapters/keybase.rs
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
use controller;
|
||||||
|
use failure::ResultExt;
|
||||||
|
use libtx::slate::Slate;
|
||||||
|
use libwallet::{Error, ErrorKind};
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::{from_str, to_string, Value};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::str::from_utf8;
|
||||||
|
use std::thread::sleep;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use {instantiate_wallet, WalletCommAdapter, WalletConfig};
|
||||||
|
|
||||||
|
const TTL: u16 = 60; // TODO: Pass this as a parameter
|
||||||
|
const SLEEP_DURATION: Duration = Duration::from_millis(5000);
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
fn api_send(payload: &str) -> Value {
|
||||||
|
let mut proc = Command::new("keybase");
|
||||||
|
proc.args(&["chat", "api", "-m", &payload]);
|
||||||
|
let output = proc.output().expect("No output").stdout;
|
||||||
|
let response: Value = from_str(from_utf8(&output).expect("Bad output")).expect("Bad output");
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all unread messages from a specific channel/topic and mark as read.
|
||||||
|
fn read_from_channel(channel: &str, topic: &str) -> Vec<String> {
|
||||||
|
let payload = to_string(&json!({
|
||||||
|
"method": "read",
|
||||||
|
"params": {
|
||||||
|
"options": {
|
||||||
|
"channel": {
|
||||||
|
"name": channel, "topic_type": "dev", "topic_name": topic
|
||||||
|
},
|
||||||
|
"unread_only": true, "peek": false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)).unwrap();
|
||||||
|
|
||||||
|
let response = api_send(&payload);
|
||||||
|
let mut unread: Vec<String> = Vec::new();
|
||||||
|
for msg in response["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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unread
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get unread messages from all channels and mark as read.
|
||||||
|
fn get_unread(topic: &str) -> HashMap<String, String> {
|
||||||
|
let payload = to_string(&json!({
|
||||||
|
"method": "list",
|
||||||
|
"params": {
|
||||||
|
"options": {
|
||||||
|
"topic_type": "dev",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)).unwrap();
|
||||||
|
let response = api_send(&payload);
|
||||||
|
|
||||||
|
let mut channels = HashSet::new();
|
||||||
|
// Unfortunately the response does not contain the message body
|
||||||
|
// and a seperate call is needed for each channel
|
||||||
|
for msg in response["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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut unread: HashMap<String, String> = HashMap::new();
|
||||||
|
for channel in channels.iter() {
|
||||||
|
let messages = read_from_channel(channel, topic);
|
||||||
|
for msg in messages {
|
||||||
|
unread.insert(msg, channel.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unread
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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!({
|
||||||
|
"method": "send",
|
||||||
|
"params": {
|
||||||
|
"options": {
|
||||||
|
"channel": {
|
||||||
|
"name": channel, "topic_name": topic, "topic_type": "dev"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"body": to_string(&message).unwrap()
|
||||||
|
},
|
||||||
|
"exploding_lifetime": seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)).unwrap();
|
||||||
|
let response = api_send(&payload);
|
||||||
|
match response["result"]["message"].as_str() {
|
||||||
|
Some("message sent") => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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();
|
||||||
|
println!("Waiting for message from {}...", channel);
|
||||||
|
while start.elapsed().as_secs() < nseconds {
|
||||||
|
let unread = read_from_channel(channel, SLATE_SIGNED);
|
||||||
|
for msg in unread.iter() {
|
||||||
|
let blob = from_str::<Slate>(msg);
|
||||||
|
match blob {
|
||||||
|
Ok(slate) => {
|
||||||
|
println!("Received message from {}", channel);
|
||||||
|
return Some(slate);
|
||||||
|
}
|
||||||
|
Err(_) => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sleep(SLEEP_DURATION);
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"Did not receive reply from {} in {} seconds",
|
||||||
|
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> {
|
||||||
|
// Send original slate to recipient with the SLATE_NEW topic
|
||||||
|
match send(slate, addr, SLATE_NEW, TTL) {
|
||||||
|
true => (),
|
||||||
|
false => return Err(ErrorKind::ClientCallback("Posting transaction slate"))?,
|
||||||
|
}
|
||||||
|
println!("Sent new slate to {}", addr);
|
||||||
|
// Wait for response from recipient with SLATE_SIGNED topic
|
||||||
|
match poll(TTL as u64, addr) {
|
||||||
|
Some(slate) => return Ok(slate),
|
||||||
|
None => return Err(ErrorKind::ClientCallback("Receiving reply from recipient"))?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
|
let wallet =
|
||||||
|
instantiate_wallet(config.clone(), passphrase, account, node_api_secret.clone())
|
||||||
|
.context(ErrorKind::WalletSeedDecryption)?;
|
||||||
|
|
||||||
|
println!("Listening for messages via keybase chat...");
|
||||||
|
loop {
|
||||||
|
// listen for messages from all channels with topic SLATE_NEW
|
||||||
|
let unread = get_unread(SLATE_NEW);
|
||||||
|
for (msg, channel) in &unread {
|
||||||
|
let blob = from_str::<Slate>(msg);
|
||||||
|
match blob {
|
||||||
|
Ok(mut slate) => {
|
||||||
|
println!("Received message from channel {}", channel);
|
||||||
|
match controller::foreign_single_use(wallet.clone(), |api| {
|
||||||
|
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 => {
|
||||||
|
println!("Returned slate to {}", channel);
|
||||||
|
}
|
||||||
|
false => {
|
||||||
|
println!("Failed to return slate to {}", channel);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error : {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sleep(SLEEP_DURATION);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,10 +14,12 @@
|
||||||
|
|
||||||
mod file;
|
mod file;
|
||||||
mod http;
|
mod http;
|
||||||
|
mod keybase;
|
||||||
mod null;
|
mod null;
|
||||||
|
|
||||||
pub use self::file::FileWalletCommAdapter;
|
pub use self::file::FileWalletCommAdapter;
|
||||||
pub use self::http::HTTPWalletCommAdapter;
|
pub use self::http::HTTPWalletCommAdapter;
|
||||||
|
pub use self::keybase::KeybaseWalletCommAdapter;
|
||||||
pub use self::null::NullWalletCommAdapter;
|
pub use self::null::NullWalletCommAdapter;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
//! Controller for wallet.. instantiates and handles listeners (or single-run
|
//! Controller for wallet.. instantiates and handles listeners (or single-run
|
||||||
//! invocations) as needed.
|
//! invocations) as needed.
|
||||||
//! Still experimental
|
//! Still experimental
|
||||||
use adapters::{FileWalletCommAdapter, HTTPWalletCommAdapter};
|
use adapters::{FileWalletCommAdapter, HTTPWalletCommAdapter, KeybaseWalletCommAdapter};
|
||||||
use api::{ApiServer, BasicAuthMiddleware, Handler, ResponseFuture, Router, TLSConfig};
|
use api::{ApiServer, BasicAuthMiddleware, Handler, ResponseFuture, Router, TLSConfig};
|
||||||
use core::core;
|
use core::core;
|
||||||
use core::core::Transaction;
|
use core::core::Transaction;
|
||||||
|
@ -346,6 +346,9 @@ where
|
||||||
let adapter = FileWalletCommAdapter::new();
|
let adapter = FileWalletCommAdapter::new();
|
||||||
adapter.send_tx_async(&args.dest, &slate)?;
|
adapter.send_tx_async(&args.dest, &slate)?;
|
||||||
api.tx_lock_outputs(&slate, lock_fn)?;
|
api.tx_lock_outputs(&slate, lock_fn)?;
|
||||||
|
} else if args.method == "keybase" {
|
||||||
|
let adapter = KeybaseWalletCommAdapter::new();
|
||||||
|
adapter.send_tx_sync(&args.dest, &slate)?;
|
||||||
} else {
|
} else {
|
||||||
error!("unsupported payment method: {}", args.method);
|
error!("unsupported payment method: {}", args.method);
|
||||||
return Err(ErrorKind::ClientCallback("unsupported payment method"))?;
|
return Err(ErrorKind::ClientCallback("unsupported payment method"))?;
|
||||||
|
|
|
@ -22,6 +22,7 @@ extern crate rand;
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
extern crate serde_derive;
|
||||||
|
#[macro_use]
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
@ -56,7 +57,8 @@ mod node_clients;
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
pub use adapters::{
|
pub use adapters::{
|
||||||
FileWalletCommAdapter, HTTPWalletCommAdapter, NullWalletCommAdapter, WalletCommAdapter,
|
FileWalletCommAdapter, HTTPWalletCommAdapter, KeybaseWalletCommAdapter, NullWalletCommAdapter,
|
||||||
|
WalletCommAdapter,
|
||||||
};
|
};
|
||||||
pub use error::{Error, ErrorKind};
|
pub use error::{Error, ErrorKind};
|
||||||
pub use libwallet::types::{BlockFees, CbData, NodeClient, WalletBackend, WalletInfo, WalletInst};
|
pub use libwallet::types::{BlockFees, CbData, NodeClient, WalletBackend, WalletInfo, WalletInst};
|
||||||
|
|
Loading…
Reference in a new issue