Skip to content

Commit

Permalink
feat: add expiration transaction policy
Browse files Browse the repository at this point in the history
  • Loading branch information
hal3e committed Jan 21, 2025
1 parent 66b3f0f commit 2a544c6
Show file tree
Hide file tree
Showing 11 changed files with 308 additions and 52 deletions.
1 change: 1 addition & 0 deletions docs/src/calling-contracts/tx-policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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.
3. **Expiration** - Block after 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.

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?;
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 succed. Block height between `maturity` and `expiration`");

Check warning on line 430 in e2e/tests/contracts.rs

View workflow job for this annotation

GitHub Actions / cargo-verifications (check_typos)

"succed" should be "succeed".
}
{
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<()> {
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 succed. Block height between `maturity` and `expiration`");

Check warning on line 1293 in e2e/tests/predicates.rs

View workflow job for this annotation

GitHub Actions / cargo-verifications (check_typos)

"succed" should be "succeed".
}
{
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 succed. Block height between `maturity` and `expiration`");

Check warning on line 321 in e2e/tests/providers.rs

View workflow job for this annotation

GitHub Actions / cargo-verifications (check_typos)

"succed" should be "succeed".
}
{
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 succed. Block height between `maturity` and `expiration`");

Check warning on line 654 in e2e/tests/scripts.rs

View workflow job for this annotation

GitHub Actions / cargo-verifications (check_typos)

"succed" should be "succeed".
}
{
provider.produce_blocks(15, None).await?;
let err = call_handler.call().await.expect_err("expiration reached");

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

Ok(())
}
Loading

0 comments on commit 2a544c6

Please sign in to comment.