forked from MystenLabs/sui
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Faucet] Implement simple faucet service (MystenLabs#1549)
- Loading branch information
Showing
11 changed files
with
617 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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::*; |
Oops, something went wrong.