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: new instruction type AssertBucketContains #1106

Merged
merged 13 commits into from
Aug 1, 2024
1,591 changes: 1,025 additions & 566 deletions applications/tari_dan_wallet_web_ui/package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions applications/tari_dan_wallet_web_ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
"@mui/x-data-grid": "^6.0.2",
"@tanstack/react-query": "^4.33.0",
"@tanstack/react-query-devtools": "^4.33.0",
"@tari-project/typescript-bindings": "file:../../bindings",
"@tari-project/wallet_daemon_client": "file:../../clients/javascript/wallet_daemon_client",
"@tari-project/typescript-bindings": "^1.0.3",
"@tari-project/wallet_jrpc_client": "^1.0.8",
"@walletconnect/core": "^2.13.3",
"@walletconnect/web3wallet": "^1.12.3",
"file-saver": "^2.0.5",
Expand Down
2 changes: 1 addition & 1 deletion applications/tari_dan_wallet_web_ui/src/utils/json_rpc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ import type {
SubstatesListRequest,
SubstatesListResponse,
} from "@tari-project/typescript-bindings/wallet-daemon-client";
import { AccountGetDefaultRequest, TemplatesGetRequest, WalletDaemonClient } from "@tari-project/wallet_daemon_client";
import { AccountGetDefaultRequest, TemplatesGetRequest, WalletDaemonClient } from "@tari-project/wallet_jrpc_client";

let clientInstance: WalletDaemonClient | null = null;
let pendingClientInstance: Promise<WalletDaemonClient> | null = null;
Expand Down
2 changes: 1 addition & 1 deletion applications/tari_dan_wallet_web_ui/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src", "../../bindings/**/*"],
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
4 changes: 2 additions & 2 deletions bindings/package-lock.json

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

2 changes: 1 addition & 1 deletion bindings/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tari-project/typescript-bindings",
"version": "1.0.3",
"version": "1.0.4",
"description": "",
"main": "index.ts",
"publishConfig": {
Expand Down
5 changes: 4 additions & 1 deletion bindings/src/types/Instruction.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Amount } from "./Amount";
import type { Arg } from "./Arg";
import type { ComponentAddress } from "./ComponentAddress";
import type { ConfidentialClaim } from "./ConfidentialClaim";
import type { LogLevel } from "./LogLevel";
import type { ResourceAddress } from "./ResourceAddress";

export type Instruction =
| { CreateAccount: { owner_public_key: string; workspace_bucket: string | null } }
Expand All @@ -12,4 +14,5 @@ export type Instruction =
| { EmitLog: { level: LogLevel; message: string } }
| { ClaimBurn: { claim: ConfidentialClaim } }
| { ClaimValidatorFees: { epoch: number; validator_public_key: string } }
| "DropAllProofsInWorkspace";
| "DropAllProofsInWorkspace"
| { AssertBucketContains: { key: Array<number>; resource_address: ResourceAddress; min_amount: Amount } };
14 changes: 3 additions & 11 deletions clients/javascript/wallet_daemon_client/package-lock.json

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

16 changes: 16 additions & 0 deletions dan_layer/engine/src/runtime/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,9 @@ pub enum RuntimeError {
NumericConversionError { details: String },
#[error("Auth callback MUST return null, but it returned non-null")]
UnexpectedNonNullInAuthHookReturn,

#[error("Assert error: {0}")]
AssertError(#[from] AssertError),
}

impl RuntimeError {
Expand All @@ -274,6 +277,19 @@ impl IsNotFoundError for RuntimeError {
}
}

#[derive(Debug, thiserror::Error)]
pub enum AssertError {
#[error("The workspace value is not a bucket")]
InvalidBucket,
#[error("Assert expected bucket to have resource {expected} but has {got}")]
InvalidResource {
expected: ResourceAddress,
got: ResourceAddress,
},
#[error("Assert expected bucket to have at least {expected} tokens but only has {got}")]
InvalidAmount { expected: Amount, got: Amount },
}

#[derive(Debug, thiserror::Error)]
pub enum TransactionCommitError {
#[error("{count} dangling buckets remain after transaction execution")]
Expand Down
36 changes: 36 additions & 0 deletions dan_layer/engine/src/runtime/impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ use tari_template_lib::{
NonFungible,
NonFungibleAddress,
NotAuthorized,
ResourceAddress,
VaultId,
VaultRef,
},
Expand All @@ -105,6 +106,7 @@ use super::{working_state::WorkingState, Runtime};
use crate::{
runtime::{
engine_args::EngineArgs,
error::AssertError,
locking::{LockError, LockedSubstate},
scope::PushCallFrame,
tracker::StateTracker,
Expand Down Expand Up @@ -2025,6 +2027,40 @@ impl<TTemplateProvider: TemplateProvider<Template = LoadedTemplate>> RuntimeInte
Ok(InvokeResult::unit())
})
},
WorkspaceAction::AssertBucketContains => {
let key: Vec<u8> = args.get(0)?;
let resource_address: ResourceAddress = args.get(1)?;
let min_amount: Amount = args.get(2)?;

// get the bucket from the workspace
let value = self.tracker.get_from_workspace(&key)?;
let bucket_id = value
.bucket_ids()
.first()
.ok_or_else(|| RuntimeError::AssertError(AssertError::InvalidBucket))?;

self.tracker.read_with(|state| {
let bucket = state.get_bucket(*bucket_id)?;

// validate the bucket resource
if *bucket.resource_address() != resource_address {
return Err(RuntimeError::AssertError(AssertError::InvalidResource {
expected: resource_address,
got: *bucket.resource_address(),
}));
}

// validate the bucket amount
if bucket.amount() < min_amount {
return Err(RuntimeError::AssertError(AssertError::InvalidAmount {
expected: min_amount,
got: bucket.amount(),
}));
}

Ok(InvokeResult::unit())
})
},
}
}

Expand Down
2 changes: 1 addition & 1 deletion dan_layer/engine/src/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ mod engine_args;
pub use crate::runtime::engine_args::EngineArgs;

mod error;
pub use error::{RuntimeError, TransactionCommitError};
pub use error::{AssertError, RuntimeError, TransactionCommitError};

mod actions;
pub use actions::*;
Expand Down
11 changes: 11 additions & 0 deletions dan_layer/engine/src/transaction/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,17 @@ impl<TTemplateProvider: TemplateProvider<Template = LoadedTemplate> + 'static> T
.claim_validator_fees(Epoch(epoch), validator_public_key)?;
Ok(InstructionResult::empty())
},
Instruction::AssertBucketContains {
key,
resource_address,
min_amount,
} => {
runtime.interface().workspace_invoke(
WorkspaceAction::AssertBucketContains,
invoke_args![key, resource_address, min_amount].into(),
)?;
Ok(InstructionResult::empty())
},
}
}

Expand Down
176 changes: 176 additions & 0 deletions dan_layer/engine/tests/asserts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Copyright 2024 The Tari Project
// SPDX-License-Identifier: BSD-3-Clause

use std::vec;

use tari_crypto::ristretto::RistrettoSecretKey;
use tari_dan_engine::runtime::{AssertError, RuntimeError};
use tari_template_lib::{
args,
models::{Amount, ComponentAddress, NonFungibleAddress, ResourceAddress},
prelude::XTR,
};
use tari_template_test_tooling::{support::assert_error::assert_reject_reason, TemplateTest};
use tari_transaction::{Instruction, Transaction};

const FAUCET_WITHDRAWAL_AMOUNT: Amount = Amount::new(1000);

struct AssertTest {
template_test: TemplateTest,
faucet_component: ComponentAddress,
faucet_resource: ResourceAddress,
account: ComponentAddress,
account_proof: NonFungibleAddress,
account_key: RistrettoSecretKey,
}

fn setup() -> AssertTest {
let mut template_test = TemplateTest::new(vec!["tests/templates/tariswap"]);

let faucet_template = template_test.get_template_address("TestFaucet");

let initial_supply = Amount(1_000_000_000_000);
let result = template_test
.execute_and_commit(
vec![Instruction::CallFunction {
template_address: faucet_template,
function: "mint".to_string(),
args: args![initial_supply],
}],
vec![template_test.get_test_proof()],
)
.unwrap();

let faucet_component: ComponentAddress = result.finalize.execution_results[0].decode().unwrap();

let faucet_resource = result
.finalize
.result
.expect("Faucet mint failed")
.up_iter()
.find_map(|(address, _)| address.as_resource_address())
.unwrap();

// Create user account to receive faucet tokens
let (account, account_proof, account_key) = template_test.create_funded_account();

AssertTest {
template_test,
faucet_component,
faucet_resource,
account,
account_proof,
account_key,
}
}

#[test]
fn successful_assert() {
let mut test: AssertTest = setup();

test.template_test.execute_expect_success(
Transaction::builder()
.call_method(test.faucet_component, "take_free_coins", args![])
.put_last_instruction_output_on_workspace("faucet_bucket")
.assert_bucket_contains("faucet_bucket", test.faucet_resource, FAUCET_WITHDRAWAL_AMOUNT)
.call_method(test.account, "deposit", args![Workspace("faucet_bucket")])
.sign(&test.account_key)
.build(),
vec![test.account_proof.clone()],
);
}

#[test]
fn it_fails_with_invalid_resource() {
let mut test: AssertTest = setup();

// we are going to assert a different resource than the faucet resource
let invalid_resource_address = XTR;

let reason = test.template_test.execute_expect_failure(
Transaction::builder()
.call_method(test.faucet_component, "take_free_coins", args![])
.put_last_instruction_output_on_workspace("faucet_bucket")
.assert_bucket_contains("faucet_bucket", invalid_resource_address, FAUCET_WITHDRAWAL_AMOUNT)
.call_method(test.account, "deposit", args![Workspace("faucet_bucket")])
.sign(&test.account_key)
.build(),
vec![test.account_proof.clone()],
);

assert_reject_reason(
reason,
RuntimeError::AssertError(AssertError::InvalidResource {
expected: invalid_resource_address,
got: test.faucet_resource,
}),
);
}

#[test]
fn it_fails_with_invalid_amount() {
let mut test: AssertTest = setup();

// we are going to assert that the faucet bucket has more tokens that it really has
let min_amount = FAUCET_WITHDRAWAL_AMOUNT + 1;

let reason = test.template_test.execute_expect_failure(
Transaction::builder()
.call_method(test.faucet_component, "take_free_coins", args![])
.put_last_instruction_output_on_workspace("faucet_bucket")
.assert_bucket_contains("faucet_bucket", test.faucet_resource, min_amount)
.call_method(test.account, "deposit", args![Workspace("faucet_bucket")])
.sign(&test.account_key)
.build(),
vec![test.account_proof.clone()],
);

assert_reject_reason(
reason,
RuntimeError::AssertError(AssertError::InvalidAmount {
expected: min_amount,
got: FAUCET_WITHDRAWAL_AMOUNT,
}),
);
}

#[test]
fn it_fails_with_invalid_bucket() {
let mut test: AssertTest = setup();

let reason = test.template_test.execute_expect_failure(
Transaction::builder()
.call_method(test.faucet_component, "take_free_coins", args![])
// we are going to assert a workspace value that is NOT a bucket
.call_method(test.account, "get_balances", args![])
.put_last_instruction_output_on_workspace("invalid_bucket")
.assert_bucket_contains("invalid_bucket", test.faucet_resource, FAUCET_WITHDRAWAL_AMOUNT)
.call_method(test.account, "deposit", args![Workspace("faucet_bucket")])
.sign(&test.account_key)
.build(),
vec![test.account_proof.clone()],
);

assert_reject_reason(reason, RuntimeError::AssertError(AssertError::InvalidBucket {}));
}

#[test]
fn it_fails_with_invalid_workspace_key() {
let mut test: AssertTest = setup();

let reason = test.template_test.execute_expect_failure(
Transaction::builder()
.call_method(test.faucet_component, "take_free_coins", args![])
.put_last_instruction_output_on_workspace("faucet_bucket")
// we are going to assert a key that does not exist in the workspace
.assert_bucket_contains("invalid_key", test.faucet_resource, FAUCET_WITHDRAWAL_AMOUNT)
.call_method(test.account, "deposit", args![Workspace("faucet_bucket")])
.sign(&test.account_key)
.build(),
vec![test.account_proof.clone()],
);

assert_reject_reason(reason, RuntimeError::ItemNotOnWorkspace {
key: "invalid_key".to_string(),
});
}
Loading
Loading