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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
use std::{ffi::OsString, fs};

use eyre::{bail, eyre};
use filament_app::accounts;
use filament_chain::{
    input::Transfer,
    Account,
    Address,
    Amount,
    Auth,
    ChainId,
    Denom,
    Input,
    Transaction,
    TransactionBody,
    REGISTRY,
};
use filament_crypto::{SignBytes, SigningKey};
use filament_encoding::{to_bytes, FromBech32 as _, ToBech32};

use crate::{
    context::Context,
    terminal::{Args, Help},
};

pub const HELP: Help = Help {
    name: "transfer",
    description: "Transfer an asset from one account to another",
    version: env!("CARGO_PKG_VERSION"),
    usage: r#"
Usage

    flt transfer <chain_id> <from> <to> <amount> <denom>

Options
    "#,
};

pub struct Options {
    chain_id: ChainId,
    from: Address,
    to: Address,
    amount: Amount,
    denom: Denom,
}

impl Args for Options {
    fn from_args(args: Vec<OsString>) -> eyre::Result<Self> {
        let mut parser = lexopt::Parser::from_args(args);
        let vals = parser.values()?.collect::<Vec<_>>();

        if vals.len() != 5 {
            bail!("expected 5 arguments got {}", vals.len());
        }

        let mut values = vals.iter();

        Ok(Self {
            chain_id: ChainId::try_from(
                values
                    .next()
                    .unwrap()
                    .to_str()
                    .ok_or(eyre!("chain_id is not UTF-8"))?,
            )?,
            from: Address::from_bech32(
                values
                    .next()
                    .unwrap()
                    .to_str()
                    .ok_or(eyre!("from is not UTF-8"))?,
            )?,
            to: Address::from_bech32(
                values
                    .next()
                    .unwrap()
                    .to_str()
                    .ok_or(eyre!("from is not UTF-8"))?,
            )?,
            amount: Amount::try_from(
                values
                    .next()
                    .unwrap()
                    .to_str()
                    .ok_or(eyre!("amount is not UTF-8"))?,
            )?,
            denom: {
                let denom = values
                    .next()
                    .unwrap()
                    .to_str()
                    .ok_or(eyre!("denom is not UTF-8"))?
                    .to_owned();
                let asset = REGISTRY
                    .by_base_denom(&denom)
                    .ok_or(eyre::eyre!("asset not found: {}", denom))?;

                asset.denom.clone()
            },
        })
    }
}

pub fn run(ctx: Context, opts: Options) -> eyre::Result<()> {
    let signing_key = {
        let config_dir = ctx.dirs.config_dir();
        let sk_path = config_dir.join("keys").join(opts.from.to_bech32()?);
        let sk_bytes = fs::read(sk_path)?;

        SigningKey::try_from(sk_bytes.as_slice())?
    };

    let verification_key = signing_key.verification_key();

    let account = ctx
        .client
        .query::<Account>(None, accounts::Query::AccountByAddress(opts.from.clone()))?;

    let transfer = Transfer {
        from: opts.from,
        to: opts.to,
        denom: opts.denom,
        amount: opts.amount,
    };

    let body = TransactionBody {
        inputs: vec![Input::Transfer(transfer)],
        chain_id: opts.chain_id,
        max_height: None,
        account_id: account.id(),
        sequence: account.sequence(),
    };

    let sign_bytes = body.sign_bytes()?;

    let tx = Transaction {
        auth: Auth::Ed25519 {
            verification_key,
            signature: signing_key.sign(&sign_bytes),
        },
        body,
    };
    let tx_bytes = to_bytes(&tx)?;
    let res = ctx.client.broadcast_tx_commit(tx_bytes)?;

    println!("{:?}", res);

    Ok(())
}

#[cfg(test)]
mod test {
    use filament_chain::{Address, Amount, ChainId, REGISTRY};
    use filament_crypto::SigningKey;
    use filament_encoding::ToBech32 as _;
    use pretty_assertions::assert_eq;
    use rand::thread_rng;

    use super::Options;
    use crate::terminal::Args as _;

    #[test]
    fn options() -> eyre::Result<()> {
        let chain_id = ChainId::try_from("inprocess-devnet")?;
        let from = Address::from(SigningKey::new(thread_rng()).verification_key());
        let to = Address::from(SigningKey::new(thread_rng()).verification_key());
        let amount = Amount::try_from("1000")?;
        let asset = REGISTRY.by_base_denom("ugm").unwrap();

        let opts = Options::from_args(vec![
            "inprocess-devnet".into(),
            from.to_bech32()?.into(),
            to.to_bech32()?.into(),
            "1000".into(),
            "ugm".into(),
        ])?;

        assert_eq!(opts.chain_id, chain_id);
        assert_eq!(opts.from, from);
        assert_eq!(opts.to, to);
        assert_eq!(opts.amount, amount);
        assert_eq!(opts.denom, asset.denom);

        Ok(())
    }
}