From ac54bfc538a48b1a214169c6840aa7f6736ff730 Mon Sep 17 00:00:00 2001 From: NikitaM Date: Fri, 10 Jul 2020 18:39:29 +0300 Subject: [PATCH] Add high level support for sending funds from multisig (#27) - Added top level command `multisig` - Added subcommand `send` to tranfer funds from multisig to recepient - Allowed to add string with purpose of payment --- Cargo.toml | 2 +- src/main.rs | 10 +- src/multisig.rs | 265 +++++++++++++++++++++++++++++++++++++++++++++++ src/voting.rs | 161 +--------------------------- tests/conf1.json | 1 + tests/conf2.json | 1 + 6 files changed, 278 insertions(+), 162 deletions(-) create mode 100644 src/multisig.rs create mode 100644 tests/conf1.json create mode 100644 tests/conf2.json diff --git a/Cargo.toml b/Cargo.toml index 2d3b9c69..97f1583d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ readme = "README.md" license = "Apache-2.0" keywords = ["TON", "SDK", "smart contract", "tonlabs"] edition = "2018" -version = "0.1.9" +version = "0.1.10" [dependencies] base64 = "0.10.1" diff --git a/src/main.rs b/src/main.rs index 3065be9f..5267c57e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,9 +24,10 @@ mod convert; mod crypto; mod deploy; mod genaddr; +mod getconfig; mod helpers; +mod multisig; mod voting; -mod getconfig; use account::get_account; use call::{call_contract, call_contract_with_msg, generate_message, parse_params, run_get_method}; @@ -36,6 +37,7 @@ use crypto::{generate_mnemonic, extract_pubkey, generate_keypair}; use deploy::deploy_contract; use genaddr::generate_address; use getconfig::query_global_config; +use multisig::{create_multisig_command, multisig_command}; use std::{env, path::PathBuf}; use voting::{create_proposal, decode_proposal, vote}; @@ -267,6 +269,7 @@ fn main_internal() -> Result <(), String> { (@arg ID: +required +takes_value "Proposal transaction id.") ) ) + (subcommand: create_multisig_command()) (@subcommand getconfig => (about: "Reads global configuration parameter with defined index.") (@arg INDEX: +required +takes_value "Parameter index.") @@ -353,6 +356,9 @@ fn main_internal() -> Result <(), String> { return proposal_decode_command(m, conf); } } + if let Some(m) = matches.subcommand_matches("multisig") { + return multisig_command(m, conf); + } if let Some(m) = matches.subcommand_matches("getconfig") { return getconfig_command(m, conf); } @@ -651,4 +657,4 @@ fn nodeid_command(matches: &ArgMatches) -> Result<(), String> { }; println!("{}", nodeid); Ok(()) -} +} \ No newline at end of file diff --git a/src/multisig.rs b/src/multisig.rs new file mode 100644 index 00000000..132fa422 --- /dev/null +++ b/src/multisig.rs @@ -0,0 +1,265 @@ +/* + * Copyright 2018-2020 TON DEV SOLUTIONS LTD. + * + * Licensed under the SOFTWARE EVALUATION License (the "License"); you may not use + * this file except in compliance with the License. + * + * 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 TON DEV software governing permissions and + * limitations under the License. + */ +use crate::crypto::{SdkClient}; +use crate::call; +use crate::config::Config; +use crate::convert; +use clap::{App, ArgMatches, SubCommand, Arg, AppSettings}; +use serde_json; + +pub const MSIG_ABI: &str = r#"{ + "ABI version": 2, + "header": ["pubkey", "time", "expire"], + "functions": [ + { + "name": "constructor", + "inputs": [ + {"name":"owners","type":"uint256[]"}, + {"name":"reqConfirms","type":"uint8"} + ], + "outputs": [ + ] + }, + { + "name": "acceptTransfer", + "inputs": [ + {"name":"payload","type":"bytes"} + ], + "outputs": [ + ] + }, + { + "name": "sendTransaction", + "inputs": [ + {"name":"dest","type":"address"}, + {"name":"value","type":"uint128"}, + {"name":"bounce","type":"bool"}, + {"name":"flags","type":"uint8"}, + {"name":"payload","type":"cell"} + ], + "outputs": [ + ] + }, + { + "name": "submitTransaction", + "inputs": [ + {"name":"dest","type":"address"}, + {"name":"value","type":"uint128"}, + {"name":"bounce","type":"bool"}, + {"name":"allBalance","type":"bool"}, + {"name":"payload","type":"cell"} + ], + "outputs": [ + {"name":"transId","type":"uint64"} + ] + }, + { + "name": "confirmTransaction", + "inputs": [ + {"name":"transactionId","type":"uint64"} + ], + "outputs": [ + ] + }, + { + "name": "isConfirmed", + "inputs": [ + {"name":"mask","type":"uint32"}, + {"name":"index","type":"uint8"} + ], + "outputs": [ + {"name":"confirmed","type":"bool"} + ] + }, + { + "name": "getParameters", + "inputs": [ + ], + "outputs": [ + {"name":"maxQueuedTransactions","type":"uint8"}, + {"name":"maxCustodianCount","type":"uint8"}, + {"name":"expirationTime","type":"uint64"}, + {"name":"minValue","type":"uint128"}, + {"name":"requiredTxnConfirms","type":"uint8"} + ] + }, + { + "name": "getTransaction", + "inputs": [ + {"name":"transactionId","type":"uint64"} + ], + "outputs": [ + {"components":[{"name":"id","type":"uint64"},{"name":"confirmationsMask","type":"uint32"},{"name":"signsRequired","type":"uint8"},{"name":"signsReceived","type":"uint8"},{"name":"creator","type":"uint256"},{"name":"index","type":"uint8"},{"name":"dest","type":"address"},{"name":"value","type":"uint128"},{"name":"sendFlags","type":"uint16"},{"name":"payload","type":"cell"},{"name":"bounce","type":"bool"}],"name":"trans","type":"tuple"} + ] + }, + { + "name": "getTransactions", + "inputs": [ + ], + "outputs": [ + {"components":[{"name":"id","type":"uint64"},{"name":"confirmationsMask","type":"uint32"},{"name":"signsRequired","type":"uint8"},{"name":"signsReceived","type":"uint8"},{"name":"creator","type":"uint256"},{"name":"index","type":"uint8"},{"name":"dest","type":"address"},{"name":"value","type":"uint128"},{"name":"sendFlags","type":"uint16"},{"name":"payload","type":"cell"},{"name":"bounce","type":"bool"}],"name":"transactions","type":"tuple[]"} + ] + }, + { + "name": "getTransactionIds", + "inputs": [ + ], + "outputs": [ + {"name":"ids","type":"uint64[]"} + ] + }, + { + "name": "getCustodians", + "inputs": [ + ], + "outputs": [ + {"components":[{"name":"index","type":"uint8"},{"name":"pubkey","type":"uint256"}],"name":"custodians","type":"tuple[]"} + ] + } + ], + "data": [ + ], + "events": [ + { + "name": "TransferAccepted", + "inputs": [ + {"name":"payload","type":"bytes"} + ], + "outputs": [ + ] + } + ] +}"#; + +pub const TRANSFER_WITH_COMMENT: &str = r#"{ + "ABI version": 1, + "functions": [ + { + "name": "transfer", + "id": "0x00000000", + "inputs": [{"name":"comment","type":"bytes"}], + "outputs": [] + } + ], + "events": [], + "data": [] +}"#; + +pub fn create_multisig_command<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("multisig") + .about("Multisignature wallet commands.") + .setting(AppSettings::AllowLeadingHyphen) + .setting(AppSettings::TrailingVarArg) + .setting(AppSettings::DontCollapseArgsInUsage) + .subcommand(SubCommand::with_name("send") + .about("Transfer funds from multisignature wallet to recepient.") + .arg(Arg::with_name("ADDRESS") + .long("--addr") + .takes_value(true) + .help("Wallet address.")) + .arg(Arg::with_name("DEST") + .long("--dest") + .takes_value(true) + .help("Recepient address.")) + .arg(Arg::with_name("VALUE") + .long("--value") + .takes_value(true) + .help("Amount of funds to transfer.")) + .arg(Arg::with_name("PURPOSE") + .long("--purpose") + .takes_value(true) + .help("Purpose of payment.")) + .arg(Arg::with_name("SIGN") + .long("--sign") + .takes_value(true) + .help("Path to keys or seed phrase."))) +} + +pub fn multisig_command(m: &ArgMatches, config: Config) -> Result<(), String> { + if let Some(m) = m.subcommand_matches("send") { + return multisig_send_command(m, config); + } + Err("unknown multisig command".to_owned()) +} + +fn multisig_send_command(matches: &ArgMatches, config: Config) -> Result<(), String> { + let address = matches.value_of("ADDRESS") + .ok_or(format!("--addr parameter is not defined"))?; + let dest = matches.value_of("DEST") + .ok_or(format!("--dst parameter is not defined"))?; + let keys = matches.value_of("SIGN") + .ok_or(format!("--sign parameter is not defined"))?; + let value = matches.value_of("VALUE") + .ok_or(format!("--value parameter is not defined"))?; + let comment = matches.value_of("PURPOSE"); + + send(config, address, dest, value, keys, comment) +} + +pub fn encode_transfer_body(text: &str) -> Result { + let text = hex::encode(text.as_bytes()); + let client = SdkClient::new(); + let abi: serde_json::Value = serde_json::from_str(TRANSFER_WITH_COMMENT).unwrap(); + client.request( + "contracts.run.body", + json!({ + "abi": abi, + "function": "transfer", + "params": json!({ + "comment": text + }), + "internal": true, + }) + ) +} + +fn send( + conf: Config, + addr: &str, + dest: &str, + value: &str, + keys: &str, + comment: Option<&str> +) -> Result<(), String> { + let body = if let Some(text) = comment { + let msg_body: serde_json::Value = + serde_json::from_str(&encode_transfer_body(text)?) + .map_err(|e| format!("failed to encode comment: {}", e))?; + + msg_body.get("bodyBase64") + .ok_or(format!(r#"internal error: "bodyBase64" not found in sdk call result"#))? + .as_str() + .ok_or(format!(r#"internal error: "bodyBase64" field is not a string"#))? + .to_owned() + } else { + "".to_owned() + }; + + let params = json!({ + "dest": dest, + "value": convert::convert_token(value)?, + "bounce": true, + "allBalance": false, + "payload": body, + }).to_string(); + + call::call_contract( + conf, + addr, + MSIG_ABI.to_string(), + "submitTransaction", + ¶ms, + Some(keys.to_owned()), + false + ) +} \ No newline at end of file diff --git a/src/voting.rs b/src/voting.rs index 77ebf9fc..74bf2fec 100644 --- a/src/voting.rs +++ b/src/voting.rs @@ -10,168 +10,11 @@ * See the License for the specific TON DEV software governing permissions and * limitations under the License. */ -use crate::crypto::{SdkClient}; use crate::config::Config; use crate::call; +use crate::multisig::{encode_transfer_body, MSIG_ABI, TRANSFER_WITH_COMMENT}; use serde_json; -use ton_client_rs::{ - TonClient -}; - -const MSIG_ABI: &str = r#"{ - "ABI version": 2, - "header": ["pubkey", "time", "expire"], - "functions": [ - { - "name": "constructor", - "inputs": [ - {"name":"owners","type":"uint256[]"}, - {"name":"reqConfirms","type":"uint8"} - ], - "outputs": [ - ] - }, - { - "name": "acceptTransfer", - "inputs": [ - {"name":"payload","type":"bytes"} - ], - "outputs": [ - ] - }, - { - "name": "sendTransaction", - "inputs": [ - {"name":"dest","type":"address"}, - {"name":"value","type":"uint128"}, - {"name":"bounce","type":"bool"}, - {"name":"flags","type":"uint8"}, - {"name":"payload","type":"cell"} - ], - "outputs": [ - ] - }, - { - "name": "submitTransaction", - "inputs": [ - {"name":"dest","type":"address"}, - {"name":"value","type":"uint128"}, - {"name":"bounce","type":"bool"}, - {"name":"allBalance","type":"bool"}, - {"name":"payload","type":"cell"} - ], - "outputs": [ - {"name":"transId","type":"uint64"} - ] - }, - { - "name": "confirmTransaction", - "inputs": [ - {"name":"transactionId","type":"uint64"} - ], - "outputs": [ - ] - }, - { - "name": "isConfirmed", - "inputs": [ - {"name":"mask","type":"uint32"}, - {"name":"index","type":"uint8"} - ], - "outputs": [ - {"name":"confirmed","type":"bool"} - ] - }, - { - "name": "getParameters", - "inputs": [ - ], - "outputs": [ - {"name":"maxQueuedTransactions","type":"uint8"}, - {"name":"maxCustodianCount","type":"uint8"}, - {"name":"expirationTime","type":"uint64"}, - {"name":"minValue","type":"uint128"}, - {"name":"requiredTxnConfirms","type":"uint8"} - ] - }, - { - "name": "getTransaction", - "inputs": [ - {"name":"transactionId","type":"uint64"} - ], - "outputs": [ - {"components":[{"name":"id","type":"uint64"},{"name":"confirmationsMask","type":"uint32"},{"name":"signsRequired","type":"uint8"},{"name":"signsReceived","type":"uint8"},{"name":"creator","type":"uint256"},{"name":"index","type":"uint8"},{"name":"dest","type":"address"},{"name":"value","type":"uint128"},{"name":"sendFlags","type":"uint16"},{"name":"payload","type":"cell"},{"name":"bounce","type":"bool"}],"name":"trans","type":"tuple"} - ] - }, - { - "name": "getTransactions", - "inputs": [ - ], - "outputs": [ - {"components":[{"name":"id","type":"uint64"},{"name":"confirmationsMask","type":"uint32"},{"name":"signsRequired","type":"uint8"},{"name":"signsReceived","type":"uint8"},{"name":"creator","type":"uint256"},{"name":"index","type":"uint8"},{"name":"dest","type":"address"},{"name":"value","type":"uint128"},{"name":"sendFlags","type":"uint16"},{"name":"payload","type":"cell"},{"name":"bounce","type":"bool"}],"name":"transactions","type":"tuple[]"} - ] - }, - { - "name": "getTransactionIds", - "inputs": [ - ], - "outputs": [ - {"name":"ids","type":"uint64[]"} - ] - }, - { - "name": "getCustodians", - "inputs": [ - ], - "outputs": [ - {"components":[{"name":"index","type":"uint8"},{"name":"pubkey","type":"uint256"}],"name":"custodians","type":"tuple[]"} - ] - } - ], - "data": [ - ], - "events": [ - { - "name": "TransferAccepted", - "inputs": [ - {"name":"payload","type":"bytes"} - ], - "outputs": [ - ] - } - ] -}"#; - -const TRANSFER_WITH_COMMENT: &str = r#"{ - "ABI version": 1, - "functions": [ - { - "name": "transfer", - "id": "0x00000000", - "inputs": [{"name":"comment","type":"bytes"}], - "outputs": [] - } - ], - "events": [], - "data": [] -}"#; - -fn encode_transfer_body(text: &str) -> Result { - let text = hex::encode(text.as_bytes()); - let client = SdkClient::new(); - let abi: serde_json::Value = serde_json::from_str(TRANSFER_WITH_COMMENT).unwrap(); - client.request( - "contracts.run.body", - json!({ - "abi": abi, - "function": "transfer", - "params": json!({ - "comment": text - }), - "internal": true, - }) - ) -} +use ton_client_rs::TonClient; pub fn create_proposal( conf: Config, diff --git a/tests/conf1.json b/tests/conf1.json new file mode 100644 index 00000000..f55a79be --- /dev/null +++ b/tests/conf1.json @@ -0,0 +1 @@ +{"url":"https://test.ton.dev","wc":0,"addr":null,"abi_path":null,"keys_path":null,"retries":10,"timeout":25000} \ No newline at end of file diff --git a/tests/conf2.json b/tests/conf2.json new file mode 100644 index 00000000..032ba1e8 --- /dev/null +++ b/tests/conf2.json @@ -0,0 +1 @@ +{"url":"https://test2.ton.dev","wc":0,"addr":null,"abi_path":null,"keys_path":null,"retries":10,"timeout":25000} \ No newline at end of file