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
|
||||
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.
|
||||
|
||||
## 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::{self, controller, display, libwallet};
|
||||
use grin_wallet::{
|
||||
instantiate_wallet, FileWalletCommAdapter, HTTPNodeClient, HTTPWalletCommAdapter, LMDBBackend,
|
||||
NullWalletCommAdapter, WalletConfig, WalletSeed,
|
||||
instantiate_wallet, FileWalletCommAdapter, HTTPNodeClient, HTTPWalletCommAdapter,
|
||||
KeybaseWalletCommAdapter, LMDBBackend, NullWalletCommAdapter, WalletConfig, WalletSeed,
|
||||
};
|
||||
use keychain;
|
||||
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("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
|
||||
.listen(
|
||||
params,
|
||||
|
@ -452,6 +458,7 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
|
|||
"http" => HTTPWalletCommAdapter::new(),
|
||||
"file" => FileWalletCommAdapter::new(),
|
||||
"self" => NullWalletCommAdapter::new(),
|
||||
"keybase" => KeybaseWalletCommAdapter::new(),
|
||||
_ => NullWalletCommAdapter::new(),
|
||||
};
|
||||
if adapter.supports_sync() {
|
||||
|
|
|
@ -198,6 +198,13 @@ fn real_main() -> i32 {
|
|||
.short("l")
|
||||
.long("port")
|
||||
.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)))
|
||||
|
||||
.subcommand(SubCommand::with_name("owner_api")
|
||||
|
@ -235,7 +242,7 @@ fn real_main() -> i32 {
|
|||
.help("Method for sending this transaction.")
|
||||
.short("m")
|
||||
.long("method")
|
||||
.possible_values(&["http", "file", "self"])
|
||||
.possible_values(&["http", "file", "self", "keybase"])
|
||||
.default_value("http")
|
||||
.takes_value(true))
|
||||
.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 http;
|
||||
mod keybase;
|
||||
mod null;
|
||||
|
||||
pub use self::file::FileWalletCommAdapter;
|
||||
pub use self::http::HTTPWalletCommAdapter;
|
||||
pub use self::keybase::KeybaseWalletCommAdapter;
|
||||
pub use self::null::NullWalletCommAdapter;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
//! Controller for wallet.. instantiates and handles listeners (or single-run
|
||||
//! invocations) as needed.
|
||||
//! Still experimental
|
||||
use adapters::{FileWalletCommAdapter, HTTPWalletCommAdapter};
|
||||
use adapters::{FileWalletCommAdapter, HTTPWalletCommAdapter, KeybaseWalletCommAdapter};
|
||||
use api::{ApiServer, BasicAuthMiddleware, Handler, ResponseFuture, Router, TLSConfig};
|
||||
use core::core;
|
||||
use core::core::Transaction;
|
||||
|
@ -346,6 +346,9 @@ where
|
|||
let adapter = FileWalletCommAdapter::new();
|
||||
adapter.send_tx_async(&args.dest, &slate)?;
|
||||
api.tx_lock_outputs(&slate, lock_fn)?;
|
||||
} else if args.method == "keybase" {
|
||||
let adapter = KeybaseWalletCommAdapter::new();
|
||||
adapter.send_tx_sync(&args.dest, &slate)?;
|
||||
} else {
|
||||
error!("unsupported payment method: {}", args.method);
|
||||
return Err(ErrorKind::ClientCallback("unsupported payment method"))?;
|
||||
|
|
|
@ -22,6 +22,7 @@ extern crate rand;
|
|||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
@ -56,7 +57,8 @@ mod node_clients;
|
|||
mod types;
|
||||
|
||||
pub use adapters::{
|
||||
FileWalletCommAdapter, HTTPWalletCommAdapter, NullWalletCommAdapter, WalletCommAdapter,
|
||||
FileWalletCommAdapter, HTTPWalletCommAdapter, KeybaseWalletCommAdapter, NullWalletCommAdapter,
|
||||
WalletCommAdapter,
|
||||
};
|
||||
pub use error::{Error, ErrorKind};
|
||||
pub use libwallet::types::{BlockFees, CbData, NodeClient, WalletBackend, WalletInfo, WalletInst};
|
||||
|
|
Loading…
Reference in a new issue