Skip to content

Commit

Permalink
feat(withdrawal): implement fa withdrawal
Browse files Browse the repository at this point in the history
  • Loading branch information
zcabter committed Sep 18, 2024
1 parent b6cc1bf commit a211452
Show file tree
Hide file tree
Showing 13 changed files with 720 additions and 73 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

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

5 changes: 3 additions & 2 deletions crates/jstz_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ derive_more.workspace = true
erased-serde.workspace = true
getrandom.workspace = true
jstz_crypto = { path = "../jstz_crypto" }
nom.workspace = true
serde.workspace = true
tezos_crypto_rs.workspace = true
tezos_data_encoding.workspace = true
tezos-smart-rollup-host.workspace = true
tezos-smart-rollup.workspace = true
tezos_data_encoding.workspace = true
nom.workspace = true

[dev-dependencies]
anyhow.workspace = true
Expand Down
39 changes: 30 additions & 9 deletions crates/jstz_core/src/kv/outbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ use tezos_smart_rollup::{
core_unsafe::MAX_OUTPUT_SIZE,
michelson::{ticket::FA2_1Ticket, MichelsonContract, MichelsonPair},
outbox::{
AtomicBatch, OutboxMessageFull, OutboxMessageTransactionBatch, OutboxQueue,
AtomicBatch, OutboxMessageFull, OutboxMessageTransaction,
OutboxMessageTransactionBatch, OutboxQueue,
},
prelude::debug_msg,
types::{Contract, Entrypoint},
};

use tezos_data_encoding::{enc::BinWriter, encoding::HasEncoding, nom::NomReader};
Expand All @@ -20,15 +22,36 @@ const PERSISTENT_OUTBOX_QUEUE_ROOT: RefPath<'static> =

const JSTZ_OUTBOX_QUEUE_META: RefPath<'static> = RefPath::assert_from(b"/outbox/meta");

type NativeWithdrawalParameters = MichelsonPair<MichelsonContract, FA2_1Ticket>;

type Withdrawal = OutboxMessageTransactionBatch<NativeWithdrawalParameters>;
type WithdrawalParameters = MichelsonPair<MichelsonContract, FA2_1Ticket>;
type Withdrawal = OutboxMessageTransactionBatch<WithdrawalParameters>;

#[derive(Debug, HasEncoding, PartialEq)]
pub enum OutboxMessage {
Withdrawal(Withdrawal),
}

impl OutboxMessage {
pub fn new_withdrawal_message(
receiver: &Contract,
destination: &Contract,
ticket: FA2_1Ticket,
entrypoint: &str,
) -> Result<OutboxMessage> {
let entrypoint = Entrypoint::try_from(entrypoint.to_string())
.map_err(|_| OutboxError::InvalidEntrypoint)?;
let parameters = MichelsonPair(MichelsonContract(receiver.clone()), ticket);
let message = OutboxMessage::Withdrawal(
vec![OutboxMessageTransaction {
entrypoint,
parameters,
destination: destination.clone(),
}]
.into(),
);
Ok(message)
}
}

impl AtomicBatch for OutboxMessage {}

impl BinWriter for OutboxMessage {
Expand All @@ -49,11 +72,7 @@ impl<'a> NomReader<'a> for OutboxMessage {

impl From<OutboxMessage> for OutboxMessageFull<OutboxMessage> {
fn from(message: OutboxMessage) -> Self {
match message {
OutboxMessage::Withdrawal(_) => {
OutboxMessageFull::AtomicTransactionBatch(message)
}
}
OutboxMessageFull::AtomicTransactionBatch(message)
}
}

Expand Down Expand Up @@ -308,6 +327,8 @@ pub enum OutboxError {
OutboxMessageSerializationError,
OutboxQueueMetaNotFound,
OutboxQueueMetaAlreadyExists,
InvalidTicketType,
InvalidEntrypoint,
}

#[cfg(test)]
Expand Down
4 changes: 4 additions & 0 deletions crates/jstz_mock/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ pub fn account2() -> jstz_crypto::public_key_hash::PublicKeyHash {
.unwrap()
}

pub fn kt1_account1() -> ContractKt1Hash {
ContractKt1Hash::try_from("KT1QgfSE4C1dX9UqrPAXjUaFQ36F9eB4nNkV").unwrap()
}

pub fn ticket_hash1() -> TicketHash {
let ticket = UnitTicket::new(
Contract::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx").unwrap(),
Expand Down
1 change: 1 addition & 0 deletions crates/jstz_proto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jstz_crypto = { path = "../jstz_crypto" }
serde.workspace = true
serde_json.workspace = true
tezos_crypto_rs.workspace = true
tezos_data_encoding.workspace = true
tezos-smart-rollup.workspace = true

[dev-dependencies]
Expand Down
130 changes: 127 additions & 3 deletions crates/jstz_proto/src/api/smart_function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,11 @@ mod test {
use serde_json::json;

use crate::{
context::account::{Account, Address, ParsedCode},
executor::smart_function::{self, register_web_apis},
context::{
account::{Account, Address, ParsedCode},
ticket_table::TicketTable,
},
executor::smart_function::{self, register_web_apis, Script},
operation::RunFunction,
};

Expand Down Expand Up @@ -326,7 +329,7 @@ mod test {
}

#[test]
fn call_system_script_from_smart_function_succeeds() {
fn host_script_withdraw_from_smart_function_succeeds() {
let mut mock_host = JstzMockHost::default();
let host = mock_host.rt();
let mut tx = Transaction::default();
Expand Down Expand Up @@ -396,4 +399,125 @@ mod test {
.expect_err("Expected error");
assert_eq!("EvalError: InsufficientFunds", error.to_string());
}

#[test]
fn host_script_fa_withdraw_from_smart_function_succeeds() {
let receiver = jstz_mock::account1();
let source = jstz_mock::account2();
let ticketer = jstz_mock::kt1_account1();
let ticketer_string = ticketer.clone();
let l1_proxy_contract = ticketer.clone();

let ticket_id = 1234;
let ticket_content = b"random ticket content".to_vec();
let json_ticket_content = json!(&ticket_content);
assert_eq!("[114,97,110,100,111,109,32,116,105,99,107,101,116,32,99,111,110,116,101,110,116]", format!("{}", json_ticket_content));
let ticket =
jstz_mock::parse_ticket(ticketer, 1, (ticket_id, Some(ticket_content)));
let ticket_hash = ticket.hash().unwrap();
let token_smart_function_intial_ticket_balance = 100;
let withdraw_amount = 90;
let mut jstz_mock_hosh = JstzMockHost::default();

let host = jstz_mock_hosh.rt();
let mut tx = Transaction::default();

// 1. Deploy our "token contract"
tx.begin();
let token_contract_code = format!(
r#"
export default (request) => {{
const url = new URL(request.url)
if (url.pathname === "/withdraw") {{
const withdrawRequest = new Request("tezos://jstz/fa-withdraw", {{
method: "POST",
headers: {{
"Content-type": "application/json",
}},
body: JSON.stringify({{
amount: {withdraw_amount},
routing_info: {{
receiver: {{ Tz1: "{receiver}" }},
proxy_l1_contract: "{l1_proxy_contract}"
}},
ticket_info: {{
id: {ticket_id},
content: {json_ticket_content},
ticketer: "{ticketer_string}"
}}
}}),
}});
return SmartFunction.call(withdrawRequest);
}}
else {{
return Response.error();
}}
}}
"#,
);
let parsed_code = ParsedCode::try_from(token_contract_code.to_string()).unwrap();
let token_smart_function =
Script::deploy(host, &mut tx, &source, parsed_code, 0).unwrap();

// 2. Add its ticket blance
TicketTable::add(
host,
&mut tx,
&token_smart_function,
&ticket_hash,
token_smart_function_intial_ticket_balance,
)
.unwrap();
tx.commit(host).unwrap();

// 3. Call the smart function
tx.begin();
let run_function = RunFunction {
uri: format!("tezos://{}/withdraw", &token_smart_function)
.try_into()
.unwrap(),
method: Method::GET,
headers: HeaderMap::new(),
body: None,
gas_limit: 1000,
};
let fake_op_hash = Blake2b::from(b"fake_op_hash".as_ref());
smart_function::run::execute(
host,
&mut tx,
&source,
run_function.clone(),
fake_op_hash,
)
.expect("Fa withdraw expected");

tx.commit(host).unwrap();

let level = host.run_level(|_| {});
let outbox = host.outbox_at(level);

assert_eq!(1, outbox.len());
tx.begin();
let balance =
TicketTable::get_balance(host, &mut tx, &token_smart_function, &ticket_hash)
.unwrap();
assert_eq!(10, balance);

// Trying a second fa withdraw should fail with insufficient funds
tx.begin();
let fake_op_hash2 = Blake2b::from(b"fake_op_hash2".as_ref());
let error = smart_function::run::execute(
host,
&mut tx,
&source,
run_function,
fake_op_hash2,
)
.expect_err("Expected error");
assert_eq!(
"EvalError: TicketTableError: InsufficientFunds",
error.to_string()
);
}
}
7 changes: 7 additions & 0 deletions crates/jstz_proto/src/context/ticket_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ impl TicketTable {
}
}

/// Adds the given `amount` from the ticket balance of `owner`
/// for the ticket `ticket_hash` and returns the account's new balance.
/// Creates the account if it doesn't exist. Fails if the addition causes
/// an overflow.
pub fn add(
rt: &mut impl Runtime,
tx: &mut Transaction,
Expand All @@ -70,6 +74,9 @@ impl TicketTable {
}
}

/// Subtracts the given `amount` from the ticket balance of `owner`
/// for the ticket `ticket_hash` and returns the account's new balance.
/// Fails if the account doesn't exist or the account has insufficient funds.
pub fn sub(
rt: &mut impl Runtime,
tx: &mut Transaction,
Expand Down
25 changes: 24 additions & 1 deletion crates/jstz_proto/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ use boa_engine::{JsError, JsNativeError};
use derive_more::{Display, Error, From};
use tezos_smart_rollup::michelson::ticket::TicketHashError;

use crate::{context::ticket_table, executor::fa_deposit};
use crate::{
context::ticket_table,
executor::{fa_deposit, fa_withdraw},
};

#[derive(Display, Debug, Error, From)]
pub enum Error {
Expand All @@ -23,14 +26,21 @@ pub enum Error {
InvalidHttpRequest,
InvalidHttpRequestBody,
InvalidHttpRequestMethod,
InvalidHeaderValue,
InvalidUri,
InvalidTicketType,
TicketTableError {
source: ticket_table::TicketTableError,
},
FaDepositError {
source: fa_deposit::FaDepositError,
},
FaWithdrawError {
source: fa_withdraw::FaWithdrawError,
},
TicketHashError(TicketHashError),
TicketAmountTooLarge,
ZeroAmountNotAllowed,
}
pub type Result<T> = std::result::Result<T, Error>;

Expand Down Expand Up @@ -80,12 +90,25 @@ impl From<Error> for JsError {
Error::FaDepositError { source } => JsNativeError::eval()
.with_message(format!("FaDepositError: {}", source))
.into(),
Error::FaWithdrawError { source } => JsNativeError::eval()
.with_message(format!("FaWithdrawError: {}", source))
.into(),
Error::TicketHashError(inner) => JsNativeError::eval()
.with_message(format!("{}", inner))
.into(),
Error::TicketAmountTooLarge => JsNativeError::eval()
.with_message("TicketAmountTooLarge")
.into(),
Error::InvalidTicketType => JsNativeError::eval()
.with_message("InvalidTicketType")
.into(),
Error::InvalidUri => JsNativeError::eval().with_message("InvalidUri").into(),
Error::InvalidHeaderValue => JsNativeError::eval()
.with_message("InvalidHeaderValue")
.into(),
Error::ZeroAmountNotAllowed => JsNativeError::eval()
.with_message("ZeroAmountNotAllowed")
.into(),
}
}
}
Expand Down
Loading

0 comments on commit a211452

Please sign in to comment.