mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-06-26 08:59:45 +00:00
feat(wallet-ffi): add vault claim ffi
This commit is contained in:
parent
ec11c2ab0b
commit
47a94ac7e4
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -10775,6 +10775,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"vault_core",
|
||||
"wallet",
|
||||
]
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ use lee::{
|
||||
use lee_core::program::DEFAULT_PROGRAM_ID;
|
||||
use log::info;
|
||||
use tempfile::tempdir;
|
||||
use wallet::account::HumanReadableAccount;
|
||||
use wallet::{account::HumanReadableAccount, program_facades::vault::Vault};
|
||||
use wallet_ffi::{
|
||||
FfiAccount, FfiAccountIdentity, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys,
|
||||
FfiPublicAccountKey, FfiTransferResult, FfiU128, WalletHandle, error,
|
||||
@ -173,6 +173,26 @@ unsafe extern "C" {
|
||||
out_result: *mut FfiTransferResult,
|
||||
) -> error::WalletFfiError;
|
||||
|
||||
fn wallet_ffi_get_vault_balance(
|
||||
handle: *mut WalletHandle,
|
||||
owner: *const FfiBytes32,
|
||||
out_balance: *mut [u8; 16],
|
||||
) -> error::WalletFfiError;
|
||||
|
||||
fn wallet_ffi_vault_claim(
|
||||
handle: *mut WalletHandle,
|
||||
owner: *const FfiBytes32,
|
||||
amount: *const [u8; 16],
|
||||
out_result: *mut FfiTransferResult,
|
||||
) -> error::WalletFfiError;
|
||||
|
||||
fn wallet_ffi_vault_claim_private(
|
||||
handle: *mut WalletHandle,
|
||||
owner: *const FfiBytes32,
|
||||
amount: *const [u8; 16],
|
||||
out_result: *mut FfiTransferResult,
|
||||
) -> error::WalletFfiError;
|
||||
|
||||
fn wallet_ffi_register_public_account(
|
||||
handle: *mut WalletHandle,
|
||||
account_id: *const FfiBytes32,
|
||||
@ -1375,3 +1395,174 @@ fn test_wallet_ffi_transfer_generic_private() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wallet_ffi_vault_balance_and_claim_public() -> Result<()> {
|
||||
let ctx = BlockingTestContext::new()?;
|
||||
let home = tempfile::tempdir()?;
|
||||
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
|
||||
|
||||
let sender = ctx.ctx().existing_public_accounts()[0];
|
||||
let owner = ctx.ctx().existing_public_accounts()[1];
|
||||
let owner_ffi: FfiBytes32 = owner.into();
|
||||
let amount: u128 = 100;
|
||||
|
||||
// Fund the owner's vault, simulating an L1 bridge deposit.
|
||||
ctx.block_on(|ctx| async move {
|
||||
Vault(ctx.wallet())
|
||||
.send_transfer(sender, owner, amount)
|
||||
.await
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
|
||||
|
||||
let vault_balance = unsafe {
|
||||
let mut out_balance: [u8; 16] = [0; 16];
|
||||
wallet_ffi_get_vault_balance(
|
||||
wallet_ffi_handle,
|
||||
&raw const owner_ffi,
|
||||
&raw mut out_balance,
|
||||
)
|
||||
.unwrap();
|
||||
u128::from_le_bytes(out_balance)
|
||||
};
|
||||
assert_eq!(vault_balance, amount);
|
||||
|
||||
let mut transfer_result = FfiTransferResult::default();
|
||||
let claim_amount: [u8; 16] = amount.to_le_bytes();
|
||||
unsafe {
|
||||
wallet_ffi_vault_claim(
|
||||
wallet_ffi_handle,
|
||||
&raw const owner_ffi,
|
||||
&raw const claim_amount,
|
||||
&raw mut transfer_result,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
|
||||
|
||||
let vault_balance_after_claim = unsafe {
|
||||
let mut out_balance: [u8; 16] = [0; 16];
|
||||
wallet_ffi_get_vault_balance(
|
||||
wallet_ffi_handle,
|
||||
&raw const owner_ffi,
|
||||
&raw mut out_balance,
|
||||
)
|
||||
.unwrap();
|
||||
u128::from_le_bytes(out_balance)
|
||||
};
|
||||
assert_eq!(vault_balance_after_claim, 0);
|
||||
|
||||
let owner_balance = unsafe {
|
||||
let mut out_balance: [u8; 16] = [0; 16];
|
||||
wallet_ffi_get_balance(
|
||||
wallet_ffi_handle,
|
||||
&raw const owner_ffi,
|
||||
true,
|
||||
&raw mut out_balance,
|
||||
)
|
||||
.unwrap();
|
||||
u128::from_le_bytes(out_balance)
|
||||
};
|
||||
assert_eq!(owner_balance, 20_000 + amount);
|
||||
|
||||
unsafe {
|
||||
wallet_ffi_free_transfer_result(&raw mut transfer_result);
|
||||
wallet_ffi_destroy(wallet_ffi_handle);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wallet_ffi_vault_balance_and_claim_private() -> Result<()> {
|
||||
let ctx = BlockingTestContext::new()?;
|
||||
let home = tempfile::tempdir()?;
|
||||
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
|
||||
|
||||
let sender = ctx.ctx().existing_public_accounts()[0];
|
||||
let owner = ctx.ctx().existing_private_accounts()[0];
|
||||
let owner_ffi: FfiBytes32 = owner.into();
|
||||
let amount: u128 = 100;
|
||||
|
||||
// Fund the owner's vault. Real deposits always land via a public transfer (the bridge
|
||||
// program crediting the vault PDA), regardless of whether the owner is private.
|
||||
ctx.block_on(|ctx| async move {
|
||||
Vault(ctx.wallet())
|
||||
.send_transfer(sender, owner, amount)
|
||||
.await
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
|
||||
|
||||
let vault_balance = unsafe {
|
||||
let mut out_balance: [u8; 16] = [0; 16];
|
||||
wallet_ffi_get_vault_balance(
|
||||
wallet_ffi_handle,
|
||||
&raw const owner_ffi,
|
||||
&raw mut out_balance,
|
||||
)
|
||||
.unwrap();
|
||||
u128::from_le_bytes(out_balance)
|
||||
};
|
||||
assert_eq!(vault_balance, amount);
|
||||
|
||||
let mut transfer_result = FfiTransferResult::default();
|
||||
let claim_amount: [u8; 16] = amount.to_le_bytes();
|
||||
unsafe {
|
||||
wallet_ffi_vault_claim_private(
|
||||
wallet_ffi_handle,
|
||||
&raw const owner_ffi,
|
||||
&raw const claim_amount,
|
||||
&raw mut transfer_result,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
|
||||
|
||||
// Sync private account local storage with onchain encrypted state
|
||||
unsafe {
|
||||
let mut current_height = 0;
|
||||
wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height).unwrap();
|
||||
wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap();
|
||||
};
|
||||
|
||||
let vault_balance_after_claim = unsafe {
|
||||
let mut out_balance: [u8; 16] = [0; 16];
|
||||
wallet_ffi_get_vault_balance(
|
||||
wallet_ffi_handle,
|
||||
&raw const owner_ffi,
|
||||
&raw mut out_balance,
|
||||
)
|
||||
.unwrap();
|
||||
u128::from_le_bytes(out_balance)
|
||||
};
|
||||
assert_eq!(vault_balance_after_claim, 0);
|
||||
|
||||
let owner_balance = unsafe {
|
||||
let mut out_balance: [u8; 16] = [0; 16];
|
||||
let _result = wallet_ffi_get_balance(
|
||||
wallet_ffi_handle,
|
||||
&raw const owner_ffi,
|
||||
false,
|
||||
&raw mut out_balance,
|
||||
);
|
||||
u128::from_le_bytes(out_balance)
|
||||
};
|
||||
assert_eq!(owner_balance, 10_000 + amount);
|
||||
|
||||
unsafe {
|
||||
wallet_ffi_free_transfer_result(&raw mut transfer_result);
|
||||
wallet_ffi_destroy(wallet_ffi_handle);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ tokio.workspace = true
|
||||
key_protocol.workspace = true
|
||||
serde_json.workspace = true
|
||||
risc0-zkvm.workspace = true
|
||||
vault_core.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
cbindgen = "0.29"
|
||||
|
||||
@ -51,6 +51,7 @@ pub mod program_deployment;
|
||||
pub mod sync;
|
||||
pub mod transfer;
|
||||
pub mod types;
|
||||
pub mod vault;
|
||||
pub mod wallet;
|
||||
|
||||
static TOKIO_RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
|
||||
|
||||
219
lez/wallet-ffi/src/vault.rs
Normal file
219
lez/wallet-ffi/src/vault.rs
Normal file
@ -0,0 +1,219 @@
|
||||
//! Bridge vault claim functions.
|
||||
//!
|
||||
//! L1 Bedrock deposits are minted into a per-owner vault PDA account by the bridge program, not
|
||||
//! directly into the owner's account (see `bridge.rs`). The owner must separately claim the
|
||||
//! deposited funds from their vault into their own account with a signed transaction.
|
||||
|
||||
use std::{ffi::CString, ptr};
|
||||
|
||||
use lee::{program::Program, AccountId};
|
||||
use wallet::program_facades::vault::Vault;
|
||||
|
||||
use crate::{
|
||||
block_on,
|
||||
error::{print_error, WalletFfiError},
|
||||
map_execution_error,
|
||||
types::{FfiBytes32, FfiTransferResult, WalletHandle},
|
||||
wallet::get_wallet,
|
||||
};
|
||||
|
||||
/// Get the claimable balance held in an account's bridge vault.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `handle`: Valid wallet handle
|
||||
/// - `owner`: The account ID whose vault balance to query
|
||||
/// - `out_balance`: Output for balance as little-endian [u8; 16]
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Success` on successful query
|
||||
/// - Error code on failure
|
||||
///
|
||||
/// # Safety
|
||||
/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
|
||||
/// - `owner` must be a valid pointer to a `FfiBytes32` struct
|
||||
/// - `out_balance` must be a valid pointer to a `[u8; 16]` array
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn wallet_ffi_get_vault_balance(
|
||||
handle: *mut WalletHandle,
|
||||
owner: *const FfiBytes32,
|
||||
out_balance: *mut [u8; 16],
|
||||
) -> WalletFfiError {
|
||||
let wrapper = match get_wallet(handle) {
|
||||
Ok(w) => w,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if owner.is_null() || out_balance.is_null() {
|
||||
print_error("Null pointer argument");
|
||||
return WalletFfiError::NullPointer;
|
||||
}
|
||||
|
||||
let wallet = match wrapper.core.lock() {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
print_error(format!("Failed to lock wallet: {e}"));
|
||||
return WalletFfiError::InternalError;
|
||||
}
|
||||
};
|
||||
|
||||
let owner_id = AccountId::new(unsafe { (*owner).data });
|
||||
let vault_id = vault_core::compute_vault_account_id(Program::vault().id(), owner_id);
|
||||
|
||||
let balance = match block_on(wallet.get_account_balance(vault_id)) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
print_error(format!("Failed to get vault balance: {e}"));
|
||||
return WalletFfiError::NetworkError;
|
||||
}
|
||||
};
|
||||
|
||||
unsafe {
|
||||
*out_balance = balance.to_le_bytes();
|
||||
}
|
||||
|
||||
WalletFfiError::Success
|
||||
}
|
||||
|
||||
/// Claim native tokens from a public owner's vault into their account.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `handle`: Valid wallet handle
|
||||
/// - `owner`: Owner account ID (must be owned by this wallet, public)
|
||||
/// - `amount`: Amount to claim as little-endian [u8; 16]
|
||||
/// - `out_result`: Output pointer for the claim result
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Success` if the claim was submitted successfully
|
||||
/// - `InsufficientFunds` if the vault doesn't have enough balance
|
||||
/// - `KeyNotFound` if the owner's signing key is not in this wallet
|
||||
/// - Error code on other failures
|
||||
///
|
||||
/// # Memory
|
||||
/// The result must be freed with `wallet_ffi_free_transfer_result()`.
|
||||
///
|
||||
/// # Safety
|
||||
/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
|
||||
/// - `owner` must be a valid pointer to a `FfiBytes32` struct
|
||||
/// - `amount` must be a valid pointer to a `[u8; 16]` array
|
||||
/// - `out_result` must be a valid pointer to a `FfiTransferResult` struct
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn wallet_ffi_vault_claim(
|
||||
handle: *mut WalletHandle,
|
||||
owner: *const FfiBytes32,
|
||||
amount: *const [u8; 16],
|
||||
out_result: *mut FfiTransferResult,
|
||||
) -> WalletFfiError {
|
||||
let wrapper = match get_wallet(handle) {
|
||||
Ok(w) => w,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if owner.is_null() || amount.is_null() || out_result.is_null() {
|
||||
print_error("Null pointer argument");
|
||||
return WalletFfiError::NullPointer;
|
||||
}
|
||||
|
||||
let wallet = match wrapper.core.lock() {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
print_error(format!("Failed to lock wallet: {e}"));
|
||||
return WalletFfiError::InternalError;
|
||||
}
|
||||
};
|
||||
|
||||
let owner_id = AccountId::new(unsafe { (*owner).data });
|
||||
let amount = u128::from_le_bytes(unsafe { *amount });
|
||||
|
||||
match block_on(Vault(&wallet).send_claim(owner_id, amount)) {
|
||||
Ok(tx_hash) => {
|
||||
let tx_hash = CString::new(tx_hash.to_string())
|
||||
.map_or(ptr::null_mut(), std::ffi::CString::into_raw);
|
||||
|
||||
unsafe {
|
||||
(*out_result).tx_hash = tx_hash;
|
||||
(*out_result).success = true;
|
||||
}
|
||||
WalletFfiError::Success
|
||||
}
|
||||
Err(e) => {
|
||||
print_error(format!("Vault claim failed: {e:?}"));
|
||||
unsafe {
|
||||
(*out_result).tx_hash = ptr::null_mut();
|
||||
(*out_result).success = false;
|
||||
}
|
||||
map_execution_error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Claim native tokens from a private owner's vault into their account.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `handle`: Valid wallet handle
|
||||
/// - `owner`: Owner account ID (must be owned by this wallet, private)
|
||||
/// - `amount`: Amount to claim as little-endian [u8; 16]
|
||||
/// - `out_result`: Output pointer for the claim result
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Success` if the claim was submitted successfully
|
||||
/// - `InsufficientFunds` if the vault doesn't have enough balance
|
||||
/// - `KeyNotFound` if the owner's signing key is not in this wallet
|
||||
/// - Error code on other failures
|
||||
///
|
||||
/// # Memory
|
||||
/// The result must be freed with `wallet_ffi_free_transfer_result()`.
|
||||
///
|
||||
/// # Safety
|
||||
/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
|
||||
/// - `owner` must be a valid pointer to a `FfiBytes32` struct
|
||||
/// - `amount` must be a valid pointer to a `[u8; 16]` array
|
||||
/// - `out_result` must be a valid pointer to a `FfiTransferResult` struct
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn wallet_ffi_vault_claim_private(
|
||||
handle: *mut WalletHandle,
|
||||
owner: *const FfiBytes32,
|
||||
amount: *const [u8; 16],
|
||||
out_result: *mut FfiTransferResult,
|
||||
) -> WalletFfiError {
|
||||
let wrapper = match get_wallet(handle) {
|
||||
Ok(w) => w,
|
||||
Err(e) => return e,
|
||||
};
|
||||
|
||||
if owner.is_null() || amount.is_null() || out_result.is_null() {
|
||||
print_error("Null pointer argument");
|
||||
return WalletFfiError::NullPointer;
|
||||
}
|
||||
|
||||
let wallet = match wrapper.core.lock() {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
print_error(format!("Failed to lock wallet: {e}"));
|
||||
return WalletFfiError::InternalError;
|
||||
}
|
||||
};
|
||||
|
||||
let owner_id = AccountId::new(unsafe { (*owner).data });
|
||||
let amount = u128::from_le_bytes(unsafe { *amount });
|
||||
|
||||
match block_on(Vault(&wallet).send_claim_private_owner(owner_id, amount)) {
|
||||
Ok((tx_hash, _shared_key)) => {
|
||||
let tx_hash = CString::new(tx_hash.to_string())
|
||||
.map_or(ptr::null_mut(), std::ffi::CString::into_raw);
|
||||
|
||||
unsafe {
|
||||
(*out_result).tx_hash = tx_hash;
|
||||
(*out_result).success = true;
|
||||
}
|
||||
WalletFfiError::Success
|
||||
}
|
||||
Err(e) => {
|
||||
print_error(format!("Vault claim failed: {e:?}"));
|
||||
unsafe {
|
||||
(*out_result).tx_hash = ptr::null_mut();
|
||||
(*out_result).success = false;
|
||||
}
|
||||
map_execution_error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1361,6 +1361,85 @@ enum WalletFfiError wallet_ffi_register_private_account(struct WalletHandle *han
|
||||
*/
|
||||
void wallet_ffi_free_transfer_result(struct FfiTransferResult *result);
|
||||
|
||||
/**
|
||||
* Get the claimable balance held in an account's bridge vault.
|
||||
*
|
||||
* # Parameters
|
||||
* - `handle`: Valid wallet handle
|
||||
* - `owner`: The account ID whose vault balance to query
|
||||
* - `out_balance`: Output for balance as little-endian [u8; 16]
|
||||
*
|
||||
* # Returns
|
||||
* - `Success` on successful query
|
||||
* - Error code on failure
|
||||
*
|
||||
* # Safety
|
||||
* - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
|
||||
* - `owner` must be a valid pointer to a `FfiBytes32` struct
|
||||
* - `out_balance` must be a valid pointer to a `[u8; 16]` array
|
||||
*/
|
||||
enum WalletFfiError wallet_ffi_get_vault_balance(struct WalletHandle *handle,
|
||||
const struct FfiBytes32 *owner,
|
||||
uint8_t (*out_balance)[16]);
|
||||
|
||||
/**
|
||||
* Claim native tokens from a public owner's vault into their account.
|
||||
*
|
||||
* # Parameters
|
||||
* - `handle`: Valid wallet handle
|
||||
* - `owner`: Owner account ID (must be owned by this wallet, public)
|
||||
* - `amount`: Amount to claim as little-endian [u8; 16]
|
||||
* - `out_result`: Output pointer for the claim result
|
||||
*
|
||||
* # Returns
|
||||
* - `Success` if the claim was submitted successfully
|
||||
* - `InsufficientFunds` if the vault doesn't have enough balance
|
||||
* - `KeyNotFound` if the owner's signing key is not in this wallet
|
||||
* - Error code on other failures
|
||||
*
|
||||
* # Memory
|
||||
* The result must be freed with `wallet_ffi_free_transfer_result()`.
|
||||
*
|
||||
* # Safety
|
||||
* - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
|
||||
* - `owner` must be a valid pointer to a `FfiBytes32` struct
|
||||
* - `amount` must be a valid pointer to a `[u8; 16]` array
|
||||
* - `out_result` must be a valid pointer to a `FfiTransferResult` struct
|
||||
*/
|
||||
enum WalletFfiError wallet_ffi_vault_claim(struct WalletHandle *handle,
|
||||
const struct FfiBytes32 *owner,
|
||||
const uint8_t (*amount)[16],
|
||||
struct FfiTransferResult *out_result);
|
||||
|
||||
/**
|
||||
* Claim native tokens from a private owner's vault into their account.
|
||||
*
|
||||
* # Parameters
|
||||
* - `handle`: Valid wallet handle
|
||||
* - `owner`: Owner account ID (must be owned by this wallet, private)
|
||||
* - `amount`: Amount to claim as little-endian [u8; 16]
|
||||
* - `out_result`: Output pointer for the claim result
|
||||
*
|
||||
* # Returns
|
||||
* - `Success` if the claim was submitted successfully
|
||||
* - `InsufficientFunds` if the vault doesn't have enough balance
|
||||
* - `KeyNotFound` if the owner's signing key is not in this wallet
|
||||
* - Error code on other failures
|
||||
*
|
||||
* # Memory
|
||||
* The result must be freed with `wallet_ffi_free_transfer_result()`.
|
||||
*
|
||||
* # Safety
|
||||
* - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
|
||||
* - `owner` must be a valid pointer to a `FfiBytes32` struct
|
||||
* - `amount` must be a valid pointer to a `[u8; 16]` array
|
||||
* - `out_result` must be a valid pointer to a `FfiTransferResult` struct
|
||||
*/
|
||||
enum WalletFfiError wallet_ffi_vault_claim_private(struct WalletHandle *handle,
|
||||
const struct FfiBytes32 *owner,
|
||||
const uint8_t (*amount)[16],
|
||||
struct FfiTransferResult *out_result);
|
||||
|
||||
/**
|
||||
* Create a new wallet with fresh storage.
|
||||
*
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user