Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(withdrawal): implement fa withdrawal #560

Merged
merged 1 commit into from
Sep 18, 2024
Merged
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
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