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
6 changes: 3 additions & 3 deletions packages/kos-sdk/src/chains/ethereum/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
pub mod address;
mod erc20;
pub mod request;
pub mod transaction;

use crate::chain::{self, BaseChain};
use crate::chains::evm20;
use crate::models::{self, BroadcastResult, PathOptions, Transaction, TransactionRaw};

use kos_crypto::keypair::KeyPair;
Expand Down Expand Up @@ -194,7 +194,7 @@ impl ETH {
match token {
Some(key) if key != "ETH" => {
let contract_address: address::Address = key.as_str().try_into()?;
let contract = erc20::get_contract_erc20();
let contract = evm20::get_contract_evm20();
let func = contract.function("balanceOf").map_err(|e| {
Error::InvalidMessage(format!("failed to get balanceOf function: {}", e))
})?;
Expand Down Expand Up @@ -252,7 +252,7 @@ impl ETH {
// Update addr_receiver for non-ETH token.
if !is_eth_token {
// update contract data for token transfer
let contract = erc20::get_contract_erc20();
let contract = evm20::get_contract_evm20();
let func = contract.function("transferFrom").map_err(|e| {
Error::InvalidMessage(format!("failed to get transferFrom function: {}", e))
})?;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use web3::ethabi::Contract;

// ERC20 contract ABI. This is a simplified ABI with only the `balanceOf` function.
const ERC20_CONTRACT_ABI: &str = r#"
const EVM20_CONTRACT_ABI: &str = r#"
[
{
"constant": true,
Expand Down Expand Up @@ -49,11 +49,34 @@ const ERC20_CONTRACT_ABI: &str = r#"
"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_erc20() -> Contract {
pub fn get_contract_evm20() -> Contract {
// Parse the ABI.
Contract::load(ERC20_CONTRACT_ABI.as_bytes()).unwrap()
Contract::load(EVM20_CONTRACT_ABI.as_bytes()).unwrap()
}
1 change: 1 addition & 0 deletions packages/kos-sdk/src/chains/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod bitcoin;
mod default;
mod ethereum;
mod evm20;
mod klever;
mod polygon;
mod tron;
Expand Down
147 changes: 137 additions & 10 deletions packages/kos-sdk/src/chains/tron/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ use std::str::FromStr;

use crate::{
chain::{self, BaseChain},
chains::ethereum::address::Address as ETHAddress,
chains::evm20,
models::{BroadcastResult, PathOptions, Transaction, TransactionRaw},
};

use kos_crypto::{keypair::KeyPair, secp256k1::Secp256k1KeyPair};
use kos_types::error::Error;
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 @@ -196,6 +200,69 @@ impl TRX {
})
}

async fn trigger_asset_transfer(
addr_sender: &address::Address,
addr_receiver: &address::Address,
amount: &BigNumber,
token: &str,
node: &str,
) -> Result<kos_proto::tron::Transaction, Error> {
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(),
};
let transaction = requests::create_asset_transfer(node, contract).await?;
Ok(transaction)
}

async fn trigger_trc20_transfer(
addr_sender: &address::Address,
addr_receiver: &address::Address,
amount: &BigNumber,
token: &str,
node: &str,
fee_limit: &i64,
) -> Result<kos_proto::tron::Transaction, Error> {
use ethabi;
use requests;

let contract = evm20::get_contract_evm20();
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::TransferOptions {
contract,
// TODO: estimate fee limit, for now use 100 TRX
fee_limit: fee_limit | 100000000,
};
let transaction = requests::create_trc20_transfer(node, extended).await?;
Ok(transaction)
}

/// create a send transaction network
#[wasm_bindgen(js_name = "send")]
pub async fn send(
Expand All @@ -211,17 +278,32 @@ impl TRX {

let options = TRX::get_options(options);

let fee_limit = options.fee_limit.unwrap_or(0);

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
let valid_address = TRX::validate_address(&token, None)?;
if valid_address {
TRX::trigger_trc20_transfer(
&addr_sender,
&addr_receiver,
&amount,
&token,
&node,
&fee_limit,
)
.await?
} else {
TRX::trigger_asset_transfer(
&addr_sender,
&addr_receiver,
&amount,
&token,
&node,
)
.await?
}
}
_ => {
let contract = kos_proto::tron::TransferContract {
Expand All @@ -233,7 +315,6 @@ impl TRX {
requests::create_transfer(&node, contract).await?
}
};

// update memo field
let tx = match options.memo {
Some(memo) => {
Expand Down Expand Up @@ -316,6 +397,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 +468,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
24 changes: 23 additions & 1 deletion packages/kos-sdk/src/chains/tron/requests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
use crate::utils;
use kos_types::error::Error;
use serde::Serialize;
use serde_json::json;

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

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 +74,21 @@ pub async fn create_asset_transfer(
create_transaction(url, contract).await
}

pub async fn create_trc20_transfer(
node_url: &str,
contract: TransferOptions,
) -> 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 +111,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
Loading