Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/gem_ton/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod tvm;
pub mod address;
pub mod constants;
pub mod models;
pub mod tonstakers;

pub use address::{Address, validate_address};
pub use primitives::AddressError;
45 changes: 45 additions & 0 deletions crates/gem_ton/src/models/rpc.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,52 @@
use std::error::Error;

use num_bigint::BigUint;
use serde::{Deserialize, Serialize};
use serde_serializers::biguint_from_hex_str;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiResult<T> {
pub ok: bool,
pub result: T,
}

#[derive(Debug, Clone, Deserialize)]
pub struct RunGetMethodResult {
pub stack: Vec<RunGetMethodStackItem>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum RunGetMethodStackItem {
Num {
value: String,
},
#[serde(other)]
Other,
}

impl RunGetMethodResult {
pub fn get_num(&self, index: usize) -> Result<BigUint, Box<dyn Error + Send + Sync>> {
match self.stack.get(index) {
Some(RunGetMethodStackItem::Num { value }) => biguint_from_hex_str(value),
_ => Err(format!("expected num at TON stack index {index}").into()),
}
}
}

#[cfg(test)]
mod tests {
use serde_json::json;

use super::*;

#[test]
fn test_get_num() {
let value = json!({ "stack": [{ "type": "num", "value": "0xff" }, { "type": "cell", "value": "..." }] });
let result: RunGetMethodResult = serde_json::from_value(value).unwrap();

assert_eq!(result.get_num(0).unwrap(), BigUint::from(255u32));
assert!(result.get_num(1).is_err());
assert!(result.get_num(2).is_err());
}
}
1 change: 0 additions & 1 deletion crates/gem_ton/src/rpc/client.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use std::error::Error;

use primitives::{Asset, AssetId, AssetType, chain::Chain};
use serde_json;

use chain_traits::{ChainAccount, ChainAddressStatus, ChainPerpetual, ChainStaking, ChainTraits};
use gem_client::{Client, ClientExt};
Expand Down
4 changes: 4 additions & 0 deletions crates/gem_ton/src/signer/chain_signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@ impl ChainSigner for TonChainSigner {
fn sign_swap(&self, input: &SignerInput, private_key: &[u8]) -> Result<Vec<String>, SignerError> {
TonSigner::new(private_key)?.sign_swap(input, None)
}

fn sign_earn(&self, input: &SignerInput, private_key: &[u8]) -> Result<Vec<String>, SignerError> {
TonSigner::new(private_key)?.sign_earn(input, None)
}
}
12 changes: 12 additions & 0 deletions crates/gem_ton/src/signer/transaction/earn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use primitives::{SignerError, SignerInput};

use super::request::TransferRequest;
use crate::{tonstakers, tvm::BagOfCells};

pub(super) fn build_request(input: &SignerInput) -> Result<TransferRequest, SignerError> {
let earn_data = input.input_type.get_earn_data()?;
let payload = BagOfCells::parse_base64_root(&earn_data.call_data)?;
let earn_type = input.input_type.get_earn_type()?;
let attached_value = tonstakers::attached_value(earn_type, &input.value)?;
TransferRequest::new_with_payload(&earn_data.contract_address, &attached_value.to_string(), input.memo.clone(), Some(payload), true, None)
}
1 change: 1 addition & 0 deletions crates/gem_ton/src/signer/transaction/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod earn;
pub(super) mod message;
pub(super) mod request;
mod sign;
Expand Down
6 changes: 6 additions & 0 deletions crates/gem_ton/src/signer/transaction/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use num_bigint::BigUint;
use primitives::{FeeOption, SignerError, SignerInput};

use super::{
earn,
message::{InternalMessage, build_internal_message},
request::{JettonTransferRequest, TransferRequest},
};
Expand Down Expand Up @@ -55,6 +56,11 @@ impl TonSigner {
Ok(vec![self.sign_requests(vec![request], input.metadata.get_sequence()?, expire_at)?])
}

pub fn sign_earn(&self, input: &SignerInput, expire_at: Option<u32>) -> Result<Vec<String>, SignerError> {
let request = earn::build_request(input)?;
Ok(vec![self.sign_requests(vec![request], input.metadata.get_sequence()?, expire_at)?])
}

pub(crate) fn sign_requests(&self, requests: Vec<TransferRequest>, sequence: u64, expire_at: Option<u32>) -> Result<String, SignerError> {
let sequence = u32::try_from(sequence).map_err(|_| SignerError::invalid_input("TON sequence does not fit in u32"))?;
let expire_at = resolve_expire_at(sequence, expire_at)?;
Expand Down
17 changes: 17 additions & 0 deletions crates/gem_ton/src/tonstakers/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
pub const STAKING_CONTRACT: &str = "EQCkWxfyhAkim3g2DjKQQg8T5P4g-Q1-K_jErGcDJZ4i-vqR";
pub const TS_TON_MASTER: &str = "0:BDF3FA8098D129B54B4F73B5BAC5D1E1FD91EB054169C3916DFC8CCD536D1000";

#[cfg(feature = "rpc")]
mod pool;

#[cfg(feature = "rpc")]
pub use pool::{PoolFullData, get_pool_full_data};

#[cfg(feature = "signer")]
mod payload;

#[cfg(feature = "signer")]
pub use payload::{build_stake_payload_base64, build_unstake_payload_base64};

#[cfg(feature = "signer")]
pub(crate) use payload::attached_value;
94 changes: 94 additions & 0 deletions crates/gem_ton/src/tonstakers/payload.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use std::str::FromStr;

use num_bigint::BigUint;
use primitives::{EarnType, SignerError};

use crate::{
Address,
tvm::{BagOfCells, CellBuilder},
};

const PARTNER_CODE: u64 = 0x000000106765cd3b;
const STAKE_OPCODE: u32 = 0x47D54391;
const STAKE_FEE: u64 = 1_000_000_000;
const UNSTAKE_OPCODE: u32 = 0x595F07BC;
const UNSTAKE_FEE: u64 = 1_050_000_000;

pub(crate) fn attached_value(earn_type: &EarnType, value: &str) -> Result<BigUint, SignerError> {
match earn_type {
EarnType::Deposit(_) => Ok(BigUint::from_str(value)? + BigUint::from(STAKE_FEE)),
Comment thread
0xh3rman marked this conversation as resolved.
EarnType::Withdraw(_) => Ok(BigUint::from(UNSTAKE_FEE)),
}
}

pub fn build_stake_payload_base64() -> Result<String, SignerError> {
let mut builder = CellBuilder::new();
builder.store_u32(32, STAKE_OPCODE)?.store_u64(64, 1)?.store_u64(64, PARTNER_CODE)?;
Ok(BagOfCells::from_root(builder.build()?).to_base64(true)?)
}

pub fn build_unstake_payload_base64(owner: &Address, amount: &BigUint) -> Result<String, SignerError> {
// Tonstakers unstake flags cell: wait_till_round_end=0, fill_or_kill=0.
let mut flags = CellBuilder::new();
flags.store_u8(2, 0)?;

let mut builder = CellBuilder::new();
builder
.store_u32(32, UNSTAKE_OPCODE)?
.store_u64(64, 0)?
.store_coins(amount)?
.store_address(owner)?
.store_bit(true)?
.store_child(flags.build()?)?;
Ok(BagOfCells::from_root(builder.build()?).to_base64(true)?)
}

#[cfg(test)]
mod tests {
use num_bigint::BigUint;
use primitives::{Asset, Chain, ContractCallData, EarnType, SignerInput, TransactionInputType, TransactionLoadMetadata, YieldProvider};

use super::{build_stake_payload_base64, build_unstake_payload_base64};
use crate::{
Address,
signer::{TonSigner, testkit::TEST_ADDRESS as TON_TEST_WALLET_ADDRESS},
};

const TEST_TON_PRIVATE_KEY: &str = "c7702dadcd00d470df27dee0ddd97fbcf9deba52b60f7dd2b296ff42bb1fcad6";

#[test]
fn test_build_stake_payload_base64() {
assert_eq!(build_stake_payload_base64().unwrap(), "te6cckEBAQEAFgAAKEfVQ5EAAAAAAAAAAQAAABBnZc07HgacMQ==");
}

#[test]
fn test_build_unstake_payload_base64() {
let owner = Address::parse(TON_TEST_WALLET_ADDRESS).unwrap();
assert_eq!(
build_unstake_payload_base64(&owner, &BigUint::from(5_000_000_000u64)).unwrap(),
"te6cckEBAgEAOQABZllfB7wAAAAAAAAAAFASoF8gCACxq4qfdwkRXv1VoZuOs5Ue3+8/kqqiDYJnNgb9gUgGjwEAASDYnkB8"
);
}

#[test]
fn test_sign_earn() {
let private_key = hex::decode(TEST_TON_PRIVATE_KEY).unwrap();
let signer = TonSigner::new(&private_key).unwrap();
let provider = YieldProvider::Tonstakers.delegation_validator(Chain::Ton);
let input_type = TransactionInputType::Earn(
Asset::from_chain(Chain::Ton),
EarnType::Deposit(provider),
ContractCallData::new(TON_TEST_WALLET_ADDRESS.to_string(), build_stake_payload_base64().unwrap()),
);
let input = SignerInput::mock_with_input_type(input_type, "", "", "10000", TransactionLoadMetadata::mock_ton(1));

let signed = signer.sign_earn(&input, Some(1_000_000_000)).unwrap();

assert_eq!(
signed,
vec![
"te6cckEBBAEAxAABRYgBkF1w67cBLG0e0D7j0y2ShzflCe2JrlAjS4pC8UHg85AMAQGc7lIsHiR1BaRlPmDkfMkHAvxvlbsJ1391Wonok5rMuLPKeH40GeEHqwp0XOlcRUdPx+rZqgah1TLhJ3662SfeDimpoxc7msoAAAAAAQADAgFoYgAsauKn3cJEV79VaGbjrOVHt/vP5Kqog2CZzYG/YFIBo6Hc14iAAAAAAAAAAAAAAAAAAQMAKEfVQ5EAAAAAAAAAAQAAABBnZc07mOCPIw==".to_string()
]
);
}
}
72 changes: 72 additions & 0 deletions crates/gem_ton/src/tonstakers/pool.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use std::error::Error;

use num_bigint::BigUint;

use gem_client::{Client, ClientExt};

use crate::models::RunGetMethodResult;
use crate::rpc::client::TonClient;

// Stack offsets in the Tonstakers pool `get_pool_full_data` get-method tuple.
// Layout source: ton-blockchain/liquid-staking-contract `compose_pool_full_data_internal`.
const POOL_FULL_DATA_TOTAL_BALANCE_INDEX: usize = 2;
const POOL_FULL_DATA_SUPPLY_INDEX: usize = 13;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PoolFullData {
pub total_balance: BigUint,
pub supply: BigUint,
}

impl PoolFullData {
pub fn from_stack(result: &RunGetMethodResult) -> Result<Self, Box<dyn Error + Send + Sync>> {
Ok(Self {
total_balance: result.get_num(POOL_FULL_DATA_TOTAL_BALANCE_INDEX)?,
supply: result.get_num(POOL_FULL_DATA_SUPPLY_INDEX)?,
})
}
}

pub async fn get_pool_full_data<C: Client>(client: &TonClient<C>, address: &str) -> Result<PoolFullData, Box<dyn Error + Send + Sync>> {
let result: RunGetMethodResult = client
.client
.post(
"/api/v3/runGetMethod",
&serde_json::json!({
"address": address,
"method": "get_pool_full_data",
"stack": [],
}),
)
.await?;
PoolFullData::from_stack(&result)
}

#[cfg(test)]
mod tests {
use serde_json::{Value, json};

use super::*;

const MAX_LOAN_PER_VALIDATOR_INDEX: usize = 10;

fn num_stack_item(value: i64) -> Value {
let hex = if value < 0 { format!("-0x{:x}", value.unsigned_abs()) } else { format!("0x{value:x}") };
json!({ "type": "num", "value": hex })
}

#[test]
fn test_pool_full_data_from_stack() {
let mut stack = vec![num_stack_item(0); POOL_FULL_DATA_SUPPLY_INDEX + 1];
stack[POOL_FULL_DATA_TOTAL_BALANCE_INDEX] = num_stack_item(55_036_943_253_694_618);
stack[MAX_LOAN_PER_VALIDATOR_INDEX] = num_stack_item(-1);
stack[POOL_FULL_DATA_SUPPLY_INDEX] = num_stack_item(50_324_580_070_537_824);

let value = json!({ "stack": stack });
let result: RunGetMethodResult = serde_json::from_value(value).unwrap();
let data = PoolFullData::from_stack(&result).unwrap();

assert_eq!(data.total_balance, BigUint::from(55_036_943_253_694_618_u64));
assert_eq!(data.supply, BigUint::from(50_324_580_070_537_824_u64));
}
}
11 changes: 11 additions & 0 deletions crates/primitives/src/contract_call_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,14 @@ pub struct ContractCallData {
pub approval: Option<ApprovalData>,
pub gas_limit: Option<String>,
}

impl ContractCallData {
pub fn new(contract_address: String, call_data: String) -> Self {
Self {
contract_address,
call_data,
approval: None,
gas_limit: None,
}
}
}
7 changes: 7 additions & 0 deletions crates/primitives/src/transaction_input_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ impl TransactionInputType {
}
}

pub fn get_earn_type(&self) -> Result<&EarnType, &'static str> {
match self {
TransactionInputType::Earn(_, earn_type, _) => Ok(earn_type),
_ => Err("expected earn transaction"),
}
}

pub fn get_stake_type(&self) -> Result<&StakeType, &'static str> {
match self {
TransactionInputType::Stake(_, stake_type) => Ok(stake_type),
Expand Down
32 changes: 32 additions & 0 deletions crates/primitives/src/yield_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,50 @@ use serde::{Deserialize, Serialize};
use strum::{AsRefStr, Display, EnumString};
use typeshare::typeshare;

use crate::{Chain, DelegationValidator, StakeProviderType};

#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, AsRefStr, EnumString, PartialEq, Eq)]
#[typeshare(swift = "Equatable, CaseIterable, Sendable")]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum YieldProvider {
Yo,
Tonstakers,
}

impl YieldProvider {
pub fn name(&self) -> &str {
match self {
Self::Yo => "Yo",
Self::Tonstakers => "Tonstakers",
}
}

pub fn delegation_validator(&self, chain: Chain) -> DelegationValidator {
DelegationValidator {
chain,
id: self.as_ref().to_string(),
name: self.name().to_string(),
is_active: true,
commission: 0.0,
apr: 0.0,
provider_type: StakeProviderType::Earn,
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_delegation_validator() {
let result = YieldProvider::Yo.delegation_validator(Chain::Base);

assert_eq!(result.id, "yo");
assert_eq!(result.name, "Yo");
assert_eq!(result.chain, Chain::Base);
assert_eq!(result.apr, 0.0);
assert_eq!(result.provider_type, StakeProviderType::Earn);
}
}
Loading
Loading