Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat send trc20 token #31

Merged
merged 14 commits into from
Jan 24, 2024
103 changes: 94 additions & 9 deletions packages/kos-sdk/src/chains/tron/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
pub mod address;
pub mod requests;
mod trc20;

use std::str::FromStr;

use crate::{
chain::{self, BaseChain},
chains::ethereum::address::Address as ETHAddress,
models::{BroadcastResult, PathOptions, Transaction, TransactionRaw},
};
use kos_crypto::{keypair::KeyPair, secp256k1::Secp256k1KeyPair};
Expand All @@ -13,6 +15,7 @@ use kos_types::hash::Hash;
use kos_types::number::BigNumber;

use wasm_bindgen::prelude::*;
use web3::{ethabi, types::U256};

#[derive(Debug, Copy, Clone)]
#[wasm_bindgen]
Expand Down Expand Up @@ -213,15 +216,51 @@ impl TRX {

let tx: kos_proto::tron::Transaction = match options.token {
Some(token) if token != "TRX" => {
// todo!() check if TRC20 transfer
let contract = kos_proto::tron::TransferAssetContract {
owner_address: addr_sender.as_bytes().to_vec(),
to_address: addr_receiver.as_bytes().to_vec(),
amount: amount.to_i64(),
asset_name: token.as_bytes().to_vec(),
};

requests::create_asset_transfer(&node, contract).await?
// Check if TRC20 transfer
if TRX::validate_address(&token, None)? {
let contract = trc20::get_contract_trc20();
let func = contract.function("transfer").map_err(|e| {
Error::InvalidMessage(format!("failed to get transferFrom function: {}", e))
})?;

let to_address = *ETHAddress::from_bytes(addr_receiver.as_tvm_bytes());
let encoded = func
.encode_input(&[
ethabi::Token::Address(to_address.into()),
ethabi::Token::Uint(
U256::from_dec_str(&amount.to_string())
.map_err(|e| Error::InvalidNumberParse(e.to_string()))?,
),
])
.map_err(|e| Error::InvalidTransaction(e.to_string()))?;
let contract_address = address::Address::from_str(&token)?;

let contract = kos_proto::tron::TriggerSmartContract {
owner_address: addr_sender.as_bytes().to_vec(),
contract_address: contract_address.as_bytes().to_vec(),
data: encoded,
call_token_value: 0,
call_value: 0,
token_id: 0,
};

let extended = requests::CreateTRC20TransferOptions {
contract,
// TODO: estimate fee limit, for now use 100 TRX
fee_limit: 100000000,
};
requests::create_trc20_tranfer(&node, extended)
.await
.unwrap()
} else {
let contract = kos_proto::tron::TransferAssetContract {
owner_address: addr_sender.as_bytes().to_vec(),
to_address: addr_receiver.as_bytes().to_vec(),
amount: amount.to_i64(),
asset_name: token.as_bytes().to_vec(),
};
requests::create_asset_transfer(&node, contract).await?
}
}
_ => {
let contract = kos_proto::tron::TransferContract {
Expand Down Expand Up @@ -316,6 +355,8 @@ impl TRX {
mod tests {
use std::assert_eq;

use crate::models::SendOptions;

use super::*;
use hex::FromHex;
use kos_types::Bytes32;
Expand Down Expand Up @@ -385,6 +426,50 @@ mod tests {
}
}

#[test]
fn test_send_trc20() {
// create TRX send options
let trx_options = kos_proto::options::TRXOptions {
token: Some("TKk6DLX1xWRKHjDhHfdyQKefnP1WUppEXB".to_string()),
..Default::default()
};

let options = SendOptions::new_tron_send_options(trx_options);

let result = tokio_test::block_on(TRX::send(
"TCwwZeH6so1X4R5kcdbKqa4GWuzF53xPqG".to_string(),
DEFAULT_ADDRESS.to_string(),
BigNumber::from(1000000),
Some(options),
None,
));

assert!(result.is_ok());
let t = result.unwrap().clone();
match t.clone().data {
Some(TransactionRaw::Tron(tx)) => {
let raw = &tx.raw_data.unwrap();
assert_eq!(raw.contract.len(), 1);
let c: kos_proto::tron::TriggerSmartContract =
kos_proto::unpack_from_option_any(&raw.contract.get(0).unwrap().parameter)
.unwrap();
let data: String = c.data.iter().map(|b| format!("{:02X}", b)).collect();
let owner_address = address::Address::from_bytes(&c.owner_address);
let contract_address = address::Address::from_bytes(&c.contract_address);
assert!(data.starts_with("A9059CBB"));
assert_eq!(
owner_address.to_string(),
"TCwwZeH6so1X4R5kcdbKqa4GWuzF53xPqG".to_string()
);
assert_eq!(
contract_address.to_string(),
"TKk6DLX1xWRKHjDhHfdyQKefnP1WUppEXB".to_string()
);
}
_ => assert!(false),
}
}

#[test]
fn test_validate_bip44() {
let default_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
Expand Down
25 changes: 24 additions & 1 deletion packages/kos-sdk/src/chains/tron/requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ use crate::utils;
use kos_types::error::Error;
use serde_json::json;

use serde::Serialize;

#[derive(Serialize)]
pub struct CreateTRC20TransferOptions {
#[serde(flatten)]
pub contract: kos_proto::tron::TriggerSmartContract,
pub fee_limit: u32,
}

pub async fn get_account(node_url: &str, address: &str) -> Result<kos_proto::tron::Account, Error> {
let url = format!("{}/wallet/getaccount", node_url);

Expand Down Expand Up @@ -66,13 +75,21 @@ pub async fn create_asset_transfer(
create_transaction(url, contract).await
}

pub async fn create_trc20_tranfer(
node_url: &str,
contract: CreateTRC20TransferOptions,
) -> Result<kos_proto::tron::Transaction, Error> {
let url = format!("{}/wallet/triggersmartcontract", node_url);

create_transaction(url, contract).await
}

async fn create_transaction(
url: String,
contract: impl serde::Serialize,
) -> Result<kos_proto::tron::Transaction, Error> {
let data = serde_json::to_string(&contract)?.as_bytes().to_vec();
let result = utils::http_post::<serde_json::Value>(url, &data).await?;

let raw_hex = unpack_result(result)?;
pack_tx(&raw_hex)
}
Expand All @@ -95,6 +112,12 @@ fn unpack_result(value: serde_json::Value) -> Result<String, Error> {
return Ok(v.to_string());
}

if let Some(transaction) = value.get("transaction") {
if let Some(v) = transaction.get("raw_data_hex").and_then(|v| v.as_str()) {
return Ok(v.to_string());
}
}

match value.get("Error") {
Some(err) => Err(Error::ReqwestError(err.to_string())),
None => Err(Error::ReqwestError("Unknown error".to_string())),
Expand Down
62 changes: 62 additions & 0 deletions packages/kos-sdk/src/chains/tron/trc20.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use web3::ethabi::Contract;

// TRC20 contract ABI. This is a simplified ABI with only the `transfer` function.
const TRC20_CONTRACT_ABI: &str = r#"
[
{
"constant":false,
"inputs":[
{
"name":"_from",
"type":"address"
},
{
"name":"_to",
"type":"address"
},
{
"name":"_value",
"type":"uint256"
}
],
"name":"transferFrom",
"outputs":[
{
"name":"",
"type":"bool"
}
],
"payable":false,
"stateMutability":"nonpayable",
"type":"function"
},
{
"constant":false,
"inputs":[
{
"name":"_to",
"type":"address"
},
{
"name":"_value",
"type":"uint256"
}
],
"name":"transfer",
"outputs":[
{
"name":"",
"type":"bool"
}
],
"payable":false,
"stateMutability":"nonpayable",
"type":"function"
}
]
"#;

pub fn get_contract_trc20() -> Contract {
// Parse the ABI.
Contract::load(TRC20_CONTRACT_ABI.as_bytes()).unwrap()
}