Skip to content

Commit

Permalink
[Faucet] Implement simple faucet service (MystenLabs#1549)
Browse files Browse the repository at this point in the history
  • Loading branch information
666lcz authored Apr 25, 2022
1 parent 69fa607 commit 0a29d0d
Show file tree
Hide file tree
Showing 11 changed files with 617 additions and 2 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"sui_programmability/adapter",
"sui_programmability/framework",
"sui_types",
"faucet",
"test_utils"
]

Expand Down
30 changes: 30 additions & 0 deletions faucet/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "sui-faucet"
version = "0.1.0"
edition = "2021"
authors = ["Mysten Labs <[email protected]>"]
license = "Apache-2.0"
publish = false

[dependencies]
anyhow = { version = "1.0.56", features = ["backtrace"] }
async-trait = "0.1.52"
axum = { version = "0.5.3" }
futures = "0.3.21"
thiserror = "1.0.30"
tokio = { version = "1.17.0", features = ["full"] }
tracing = { version = "0.1.31", features = ["log"] }
tracing-subscriber = { version = "0.3.9", features = ["time", "registry", "env-filter"] }
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.79"
tower = { version = "0.4", features = ["util", "timeout", "load-shed", "limit"] }

sui = { path = "../sui" }
sui-types = { path = "../sui_types" }

[dev-dependencies]
tempfile = "3.3.0"

[[bin]]
name = "sui-faucet"
path = "src/main.rs"
22 changes: 22 additions & 0 deletions faucet/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum FaucetError {
#[error("Faucet does not have enough balance")]
InsuffientBalance,

#[error("Faucet needs at least {0} coins, but only has {1} coin")]
InsuffientCoins(usize, usize),

#[error("Wallet Error: `{0}`")]
Wallet(String),

#[error("Coin Transfer Failed `{0}`")]
Transfer(String),

#[error("Internal error: {0}")]
Internal(String),
}
77 changes: 77 additions & 0 deletions faucet/src/faucet/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
use crate::FaucetError;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use sui_types::{
base_types::{ObjectID, SuiAddress},
gas_coin::GasCoin,
object::Object,
};

mod simple_faucet;
pub use self::simple_faucet::SimpleFaucet;

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FaucetReceipt {
pub sent: Vec<CoinInfo>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CoinInfo {
pub amount: u64,
pub id: ObjectID,
}

#[async_trait]
pub trait Faucet {
/// Send `Coin<SUI>` of the specified amount to the recipient
async fn send(
&self,
recipient: SuiAddress,
amounts: &[u64],
) -> Result<FaucetReceipt, FaucetError>;
}

impl From<Vec<Object>> for FaucetReceipt {
fn from(v: Vec<Object>) -> Self {
Self {
sent: v.iter().map(|c| c.into()).collect(),
}
}
}

impl From<&Object> for CoinInfo {
fn from(v: &Object) -> Self {
let gas_coin = GasCoin::try_from(v).unwrap();
Self {
amount: gas_coin.value(),
id: *gas_coin.id(),
}
}
}

#[cfg(test)]
mod tests {
use crate::setup_network_and_wallet;

use super::*;

#[tokio::test]
async fn simple_faucet_basic_interface_should_work() {
let (network, context, _address) = setup_network_and_wallet().await.unwrap();
let faucet = SimpleFaucet::new(context).await.unwrap();
test_basic_interface(faucet).await;
network.kill().await.unwrap();
}

async fn test_basic_interface(faucet: impl Faucet) {
let recipient = SuiAddress::random_for_testing_only();
let amounts = vec![1, 2, 3];

let FaucetReceipt { sent } = faucet.send(recipient, &amounts).await.unwrap();
let mut actual_amounts: Vec<u64> = sent.iter().map(|c| c.amount).collect();
actual_amounts.sort_unstable();
assert_eq!(actual_amounts, amounts);
}
}
179 changes: 179 additions & 0 deletions faucet/src/faucet/simple_faucet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use anyhow::anyhow;
use async_trait::async_trait;
use sui::wallet_commands::WalletContext;
use sui_types::{
base_types::{ObjectID, SuiAddress},
gas_coin::GasCoin,
messages::{ExecutionStatus, Transaction},
object::Object,
};
use tracing::info;

use crate::{Faucet, FaucetError, FaucetReceipt};

/// A naive implementation of a faucet that processes
/// request sequentially
pub struct SimpleFaucet {
wallet: WalletContext,
// TODO: use a queue of coins to improve concurrency
/// Used to provide fund to users
primary_coin_id: ObjectID,
/// Pay for the gas incurred in operations such as
/// transfer and split(as opposed to sending to users)
gas_coin_id: ObjectID,
active_address: SuiAddress,
}

const DEFAULT_GAS_BUDGET: u64 = 1000;

impl SimpleFaucet {
pub async fn new(mut wallet: WalletContext) -> Result<Self, FaucetError> {
let active_address = wallet.active_address().unwrap();
info!("SimpleFaucet::new with active address: {active_address}");

let mut coins = wallet
.gas_objects(active_address)
.await
.map_err(|e| FaucetError::Wallet(e.to_string()))?
.iter()
// Ok to unwrap() since `get_gas_objects` guarantees gas
.map(|q| GasCoin::try_from(&q.1).unwrap())
.collect::<Vec<GasCoin>>();
coins.sort_by_key(|a| a.value());

if coins.len() < 2 {
return Err(FaucetError::InsuffientCoins(2, coins.len()));
}

let primary_coin = &coins[coins.len() - 1];
let gas_coin = &coins[coins.len() - 2];

info!(
"Using {} as primary, {} as the gas payment",
primary_coin, gas_coin
);

Ok(Self {
wallet,
primary_coin_id: *primary_coin.id(),
gas_coin_id: *gas_coin.id(),
active_address,
})
}

async fn get_coins(&self, amounts: &[u64]) -> Result<Vec<Object>, FaucetError> {
let result = self
.split_coins(
amounts,
self.primary_coin_id,
self.gas_coin_id,
self.active_address,
DEFAULT_GAS_BUDGET,
)
.await
.map_err(|err| FaucetError::Wallet(err.to_string()))?;

Ok(result)
}

async fn transfer_coins(
&self,
coins: &[ObjectID],
recipient: SuiAddress,
) -> Result<(), FaucetError> {
for coin_id in coins.iter() {
self.transfer_coin(
*coin_id,
self.gas_coin_id,
self.active_address,
recipient,
DEFAULT_GAS_BUDGET,
)
.await
.map_err(|err| FaucetError::Transfer(err.to_string()))?;
}
Ok(())
}

async fn split_coins(
&self,
amounts: &[u64],
coin_id: ObjectID,
gas_object_id: ObjectID,
signer: SuiAddress,
budget: u64,
) -> Result<Vec<Object>, anyhow::Error> {
// TODO: move this function to impl WalletContext{} and reuse in wallet_commands
let context = &self.wallet;
let data = context
.gateway
.split_coin(
signer,
coin_id,
amounts.to_vec().clone(),
gas_object_id,
budget,
)
.await?;
let signature = context
.keystore
.read()
.unwrap()
.sign(&signer, &data.to_bytes())?;
let response = context
.gateway
.execute_transaction(Transaction::new(data, signature))
.await?
.to_split_coin_response()?
.new_coins;
Ok(response)
}

async fn transfer_coin(
&self,
coin_id: ObjectID,
gas_object_id: ObjectID,
signer: SuiAddress,
recipient: SuiAddress,
budget: u64,
) -> Result<(), anyhow::Error> {
let context = &self.wallet;

let data = context
.gateway
.transfer_coin(signer, coin_id, gas_object_id, budget, recipient)
.await?;
let signature = context
.keystore
.read()
.unwrap()
.sign(&signer, &data.to_bytes())?;
let (_cert, effects) = context
.gateway
.execute_transaction(Transaction::new(data, signature))
.await?
.to_effect_response()?;

if matches!(effects.status, ExecutionStatus::Failure { .. }) {
return Err(anyhow!("Error transferring object: {:#?}", effects.status));
}
Ok(())
}
}

#[async_trait]
impl Faucet for SimpleFaucet {
async fn send(
&self,
recipient: SuiAddress,
amounts: &[u64],
) -> Result<FaucetReceipt, FaucetError> {
let coins = self.get_coins(amounts).await?;
let coin_ids = coins.iter().map(|c| c.id()).collect::<Vec<ObjectID>>();
self.transfer_coins(&coin_ids, recipient).await?;
Ok(coins.into())
}
}
18 changes: 18 additions & 0 deletions faucet/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

mod errors;
mod faucet;
mod requests;
mod responses;

pub use errors::FaucetError;
pub use faucet::*;
pub use requests::*;
pub use responses::*;

#[cfg(test)]
mod test_utils;

#[cfg(test)]
pub use test_utils::*;
Loading

0 comments on commit 0a29d0d

Please sign in to comment.