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: add expiration transaction policy #1583

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
5 changes: 3 additions & 2 deletions docs/src/calling-contracts/tx-policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ Where:
1. **Tip** - amount to pay the block producer to prioritize the transaction.
2. **Witness Limit** - The maximum amount of witness data allowed for the transaction.
3. **Maturity** - Block until which the transaction cannot be included.
4. **Max Fee** - The maximum fee payable by this transaction.
5. **Script Gas Limit** - The maximum amount of gas the transaction may consume for executing its script code.
4. **Expiration** - Block after which the transaction cannot be included.
5. **Max Fee** - The maximum fee payable by this transaction.
6. **Script Gas Limit** - The maximum amount of gas the transaction may consume for executing its script code.

When the **Script Gas Limit** is not set, the Rust SDK will estimate the consumed gas in the background and set it as the limit.

Expand Down
2 changes: 1 addition & 1 deletion docs/src/custom-transactions/transaction-builders.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ We need to do one more thing before we stop thinking about transaction inputs. E

> **Note** It is recommended to add signers before calling `adjust_for_fee()` as the estimation will include the size of the witnesses.

We can also define transaction policies. For example, we can limit the gas price by doing the following:
We can also define transaction policies. For example, we can set the maturity and expiration with:

```rust,ignore
{{#include ../../../examples/cookbook/src/lib.rs:custom_tx_policies}}
Expand Down
76 changes: 48 additions & 28 deletions e2e/tests/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,37 +384,57 @@ async fn mult_call_has_same_estimated_and_used_gas() -> Result<()> {
}

#[tokio::test]
async fn contract_method_call_respects_maturity() -> Result<()> {
setup_program_test!(
Wallets("wallet"),
Abigen(Contract(
name = "BlockHeightContract",
project = "e2e/sway/contracts/transaction_block_height"
)),
Deploy(
name = "contract_instance",
contract = "BlockHeightContract",
wallet = "wallet",
random_salt = false,
),
);
async fn contract_method_call_respects_maturity_and_expiration() -> Result<()> {
abigen!(Contract(
name = "MyContract",
abi = "e2e/sway/contracts/transaction_block_height/out/release/transaction_block_height-abi.json"
));

let call_w_maturity = |maturity| {
contract_instance
.methods()
.calling_this_will_produce_a_block()
.with_tx_policies(TxPolicies::default().with_maturity(maturity))
};
let wallet = launch_provider_and_get_wallet().await?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why move away from setup_program_test?

let provider = wallet.try_provider()?.clone();

call_w_maturity(1).call().await.expect(
"should have passed since we're calling with a maturity \
that is less or equal to the current block height",
);
let contract_id = Contract::load_from(
"sway/contracts/transaction_block_height/out/release/transaction_block_height.bin",
LoadConfiguration::default(),
)?
.deploy_if_not_exists(&wallet, TxPolicies::default())
.await?;

call_w_maturity(3).call().await.expect_err(
"should have failed since we're calling with a maturity \
that is greater than the current block height",
);
let contract_instance = MyContract::new(contract_id, wallet);
let maturity = 10;
let expiration = 20;
let call_handler = contract_instance
.methods()
.calling_this_will_produce_a_block()
.with_tx_policies(
TxPolicies::default()
.with_maturity(maturity)
.with_expiration(expiration),
);

{
let err = call_handler
.clone()
.call()
.await
.expect_err("maturity not reached");

assert!(err.to_string().contains("TransactionMaturity"));
}
{
provider.produce_blocks(15, None).await?;
call_handler
.clone()
.call()
.await
.expect("should succeed. Block height between `maturity` and `expiration`");
}
{
provider.produce_blocks(15, None).await?;
let err = call_handler.call().await.expect_err("expiration reached");

assert!(err.to_string().contains("TransactionExpiration"));
}

Ok(())
}
Expand Down
77 changes: 77 additions & 0 deletions e2e/tests/predicates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1245,3 +1245,80 @@ async fn predicate_configurables_in_blobs() -> Result<()> {

Ok(())
}

#[tokio::test]
async fn predicate_transfer_respects_maturity_and_expiration() -> Result<()> {
hal3e marked this conversation as resolved.
Show resolved Hide resolved
abigen!(Predicate(
name = "MyPredicate",
abi = "e2e/sway/predicates/basic_predicate/out/release/basic_predicate-abi.json"
));

let predicate_data = MyPredicateEncoder::default().encode_data(4097, 4097)?;

let mut predicate: Predicate =
Predicate::load_from("sway/predicates/basic_predicate/out/release/basic_predicate.bin")?
.with_data(predicate_data);

let num_coins = 4;
let num_messages = 8;
let amount = 16;
let (provider, predicate_balance, receiver, receiver_balance, asset_id, _) =
setup_predicate_test(predicate.address(), num_coins, num_messages, amount).await?;

predicate.set_provider(provider.clone());

let maturity = 10;
let expiration = 20;
let tx_policies = TxPolicies::default()
.with_maturity(maturity)
.with_expiration(expiration);
let amount_to_send = 10;

// TODO: https://github.com/FuelLabs/fuels-rs/issues/1394
let expected_fee = 1;

{
let err = predicate
.transfer(receiver.address(), amount_to_send, asset_id, tx_policies)
.await
.expect_err("maturity not reached");

assert!(err.to_string().contains("TransactionMaturity"));
}
{
provider.produce_blocks(15, None).await?;
predicate
.transfer(receiver.address(), amount_to_send, asset_id, tx_policies)
.await
.expect("should succeed. Block height between `maturity` and `expiration`");
}
{
provider.produce_blocks(15, None).await?;
let err = predicate
.transfer(receiver.address(), amount_to_send, asset_id, tx_policies)
.await
.expect_err("expiration reached");

assert!(err.to_string().contains("TransactionExpiration"));
}

// The predicate has spent the funds
assert_address_balance(
predicate.address(),
&provider,
asset_id,
predicate_balance - amount_to_send - expected_fee,
)
.await;

// Funds were transferred
assert_address_balance(
receiver.address(),
&provider,
asset_id,
receiver_balance + amount_to_send,
)
.await;

Ok(())
}
53 changes: 34 additions & 19 deletions e2e/tests/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,38 +283,51 @@ async fn can_retrieve_latest_block_time() -> Result<()> {
}

#[tokio::test]
async fn contract_deployment_respects_maturity() -> Result<()> {
async fn contract_deployment_respects_maturity_and_expiration() -> Result<()> {
abigen!(Contract(name="MyContract", abi="e2e/sway/contracts/transaction_block_height/out/release/transaction_block_height-abi.json"));

let wallets =
launch_custom_provider_and_get_wallets(WalletsConfig::default(), None, None).await?;
let wallet = &wallets[0];
let provider = wallet.try_provider()?;
let wallet = launch_provider_and_get_wallet().await?;
let provider = wallet.try_provider()?.clone();

let deploy_w_maturity = |maturity| {
let maturity = 10;
let expiration = 20;

let deploy_w_maturity_and_expiration = || {
Contract::load_from(
"sway/contracts/transaction_block_height/out/release/transaction_block_height.bin",
LoadConfiguration::default(),
)
.map(|loaded_contract| {
loaded_contract
.deploy_if_not_exists(wallet, TxPolicies::default().with_maturity(maturity))
loaded_contract.deploy(
&wallet,
TxPolicies::default()
.with_maturity(maturity)
.with_expiration(expiration),
)
})
};

let err = deploy_w_maturity(1)?.await.expect_err(
"should not deploy contract since block height `0` is less than the requested maturity `1`",
);
{
let err = deploy_w_maturity_and_expiration()?
.await
.expect_err("maturity not reached");

let Error::Provider(s) = err else {
panic!("expected `Validation`, got: `{err}`");
};
assert!(s.contains("TransactionMaturity"));
assert!(err.to_string().contains("TransactionMaturity"));
}
{
provider.produce_blocks(15, None).await?;
deploy_w_maturity_and_expiration()?
.await
.expect("should succeed. Block height between `maturity` and `expiration`");
}
{
provider.produce_blocks(15, None).await?;
let err = deploy_w_maturity_and_expiration()?
.await
.expect_err("expiration reached");

provider.produce_blocks(1, None).await?;
deploy_w_maturity(1)?
.await
.expect("Should deploy contract since maturity `1` is <= than the block height `1`");
assert!(err.to_string().contains("TransactionExpiration"));
}

Ok(())
}
Expand Down Expand Up @@ -1087,12 +1100,14 @@ async fn tx_respects_policies() -> Result<()> {
let tip = 22;
let witness_limit = 1000;
let maturity = 4;
let expiration = 128;
let max_fee = 10_000;
let script_gas_limit = 3000;
let tx_policies = TxPolicies::new(
Some(tip),
Some(witness_limit),
Some(maturity),
Some(expiration),
Some(max_fee),
Some(script_gas_limit),
);
Expand Down
47 changes: 47 additions & 0 deletions e2e/tests/scripts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -615,3 +615,50 @@ async fn loader_can_be_presented_as_a_normal_script_with_shifted_configurables()

Ok(())
}

#[tokio::test]
async fn script_call_respects_maturity_and_expiration() -> Result<()> {
abigen!(Script(
name = "MyScript",
abi = "e2e/sway/scripts/basic_script/out/release/basic_script-abi.json"
));
let wallet = launch_provider_and_get_wallet().await.expect("");
let provider = wallet.try_provider()?.clone();
let bin_path = "sway/scripts/basic_script/out/release/basic_script.bin";

let script_instance = MyScript::new(wallet, bin_path);

let maturity = 10;
let expiration = 20;
let call_handler = script_instance.main(1, 2).with_tx_policies(
TxPolicies::default()
.with_maturity(maturity)
.with_expiration(expiration),
);

{
let err = call_handler
.clone()
.call()
.await
.expect_err("maturity not reached");

assert!(err.to_string().contains("TransactionMaturity"));
}
{
provider.produce_blocks(15, None).await?;
call_handler
.clone()
.call()
.await
.expect("should succeed. Block height between `maturity` and `expiration`");
}
{
provider.produce_blocks(15, None).await?;
let err = call_handler.call().await.expect_err("expiration reached");

assert!(err.to_string().contains("TransactionExpiration"));
}

Ok(())
}
Loading
Loading