Repository: OpenZeppelin/stylus-test-helpers
Pull request #72: feat: Expose a Signer from `Account`
Author: login: 0xNeshi
Created: 2025-03-12T12:05:48Z | Merged: 2025-03-17T07:43:14Z

<!--
Thank you for your interest in contributing to OpenZeppelin!

Consider opening an issue for discussion prior to submitting a PR. New features will be merged faster if they were first discussed and designed with the team.

Describe the changes introduced in this pull request. Include any context necessary for understanding the PR's purpose.
-->

<!-- Fill in with issue number -->
Resolves #69

#### PR Checklist

<!--
Before merging the pull request all of the following must be completed.
Feel free to submit a PR or Draft PR even if some items are pending.
Some of the items may not apply.
-->

- [x] Tests
- [x] Documentation
- [x] Changelog


Linked issue #69: [Feature]: Expose a Signer from `Account`
### What is the feature you would like to see?

We need this to be able to sign messages in tests that we can use to [write tests for `Erc20Permit`](https://github.com/OpenZeppelin/rust-contracts-stylus/issues/575).

We have something similar to this in [_lib/e2e_ in our contracts library](https://github.com/OpenZeppelin/rust-contracts-stylus/blob/0cbf8fc1c8a94ee322966f40f3748995055c66c4/lib/e2e/src/account.rs#L54-L72).

The most straightforward approach is to update [Account](https://github.com/OpenZeppelin/stylus-test-helpers/blob/2dde3ad29136fc499ab46c5518569dd5e066632f/crates/motsu/src/context.rs#L756-L760) to be a wrapper around [PrivateKeySigner](https://docs.rs/alloy-signer-local/latest/alloy_signer_local/type.PrivateKeySigner.html) (the same one we use in e2e).

But there are some implications:
1. `Account` becomes un-copiable, so to reuse the same account to invoke `contract.sender(...)` one must repeatedly call `.clone()` on the account.
2. `Account::new_at(address: Address)` must be removed, as it wouldn't be possible to efficiently derive a private key from the address. 

Solution to 1) is to store only `private_key` and `address` in `Account`, making it copiable again. The signer can be calculated ad-hoc when `sign_hash` or `sign_message` are needed.

There doesn't seem to be a solution for 2) though. Luckily there's a workaround for this. If one would need to know a specific address at which an account would be deployed, we could implement a `FromPrivateKey` trait for `Address` or just reuse the `FromTag` trait implemented in https://github.com/OpenZeppelin/stylus-test-helpers/pull/62/files#diff-ced01b2c555b639e21eb87ee2504510248dd67de0b0ca5d720243942b15c0fd9R970-R977. The flow to "precalculate" the account address would then be:
1. **Convert a private key (tag) into `Address`** <- the dev now knows the address.
2. **Instantiate an account with the same private key** <- the account is sure to have the address from before.

It is questionable whether it is even useful to know the address where an account would be deployed 🤔 

@bidzyyys @qalisander wdyt?



### Contribution Guidelines

- [x] I agree to follow this project's Contribution Guidelines
Linked issue comments: user: login: qalisander
created_at: 2025-03-11T12:24:59Z
body: We can add another mapping `Address -> PrivateKey` that we can use for sign cases. And during key generation just derive key pair from seed `str` and store only `Address` inside `Account`.
author_association: MEMBER
user: login: 0xNeshi
created_at: 2025-03-11T12:41:44Z
body: > We can add another mapping `Address -> PrivateKey` that we can use for sign cases. And during key generation just derive key pair from seed `str` and store only `Address` inside `Account`.

Would be cleaner and easier to maintain to store the private key (if not the whole signer) within `Account`.

Tbh this seems like a quickfix, but it might be worth a try just to have something.
author_association: COLLABORATOR
user: login: qalisander
created_at: 2025-03-11T12:49:28Z
body: > Tbh this seems like a quickfix

It's just a test framework. We shouldn't care much about performance at the moment and especially for rarely used features 
author_association: MEMBER
user: login: bidzyyys
created_at: 2025-03-11T13:08:32Z
body: > > We can add another mapping `Address -> PrivateKey` that we can use for sign cases. And during key generation just derive key pair from seed `str` and store only `Address` inside `Account`.
> 
> Would be cleaner and easier to maintain to store the private key (if not the whole signer) within `Account`.
> 
> Tbh this seems like a quickfix, but it might be worth a try just to have something.

If we want to store private key with an account (and I believe it may be needed at some point) we can store just fixed-size array of bytes (priv key). With this attitude it is still `Copy` and also you can create your private key from bytes when needed. It should not be a significant performance downside in unit tests in comparison to our E2E scenarios 😅 
author_association: COLLABORATOR
user: login: qalisander
created_at: 2025-03-11T13:40:59Z
body: > we can store just fixed-size array of bytes (priv key)

We can store `B256` inside `Account` and convert it to `PrivateKeySigner` and `Address`. It will look a bit obscure, but also solves the problem
author_association: MEMBER
user: login: 0xNeshi
created_at: 2025-03-12T07:17:46Z
body: > > Tbh this seems like a quickfix
> 
> It's just a test framework. We shouldn't care much about performance at the moment and especially for rarely used features

Makes sense
author_association: COLLABORATOR

Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3164d65..af583b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,10 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
-- Added ability to set the chain ID in tests #67
+- `Account` can be created from string and byte seeds, and can return a signer. #70
+- Implemented `Balance` trait for `Account` and `Contract<T>`. #70
+- Added ability to set the chain ID in tests. #67
 
 ### Changed (Breaking)
 
+- `Account` can no longer be created at a predetermined address. #70
+- `Funding::balance` is removed. #70
 - Bump Stylus SDK to v0.8.3-rc.1 #75
 
 ## [0.6.0] - 2025-03-11
diff --git a/Cargo.lock b/Cargo.lock
index 13da501..a614440 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -11,6 +11,144 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "alloy-consensus"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69e32ef5c74bbeb1733c37f4ac7f866f8c8af208b7b4265e21af609dcac5bd5e"
+dependencies = [
+ "alloy-eips",
+ "alloy-primitives",
+ "alloy-rlp",
+ "alloy-serde",
+ "alloy-trie",
+ "auto_impl",
+ "c-kzg",
+ "derive_more",
+ "serde",
+]
+
+[[package]]
+name = "alloy-consensus-any"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa13b7b1e1e3fedc42f0728103bfa3b4d566d3d42b606db449504d88dbdbdcf"
+dependencies = [
+ "alloy-consensus",
+ "alloy-eips",
+ "alloy-primitives",
+ "alloy-rlp",
+ "alloy-serde",
+ "serde",
+]
+
+[[package]]
+name = "alloy-eip2124"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "675264c957689f0fd75f5993a73123c2cc3b5c235a38f5b9037fe6c826bfb2c0"
+dependencies = [
+ "alloy-primitives",
+ "alloy-rlp",
+ "crc",
+ "thiserror",
+]
+
+[[package]]
+name = "alloy-eip2930"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0069cf0642457f87a01a014f6dc29d5d893cd4fd8fddf0c3cdfad1bb3ebafc41"
+dependencies = [
+ "alloy-primitives",
+ "alloy-rlp",
+ "serde",
+]
+
+[[package]]
+name = "alloy-eip7702"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b15b13d38b366d01e818fe8e710d4d702ef7499eacd44926a06171dd9585d0c"
+dependencies = [
+ "alloy-primitives",
+ "alloy-rlp",
+ "serde",
+ "thiserror",
+]
+
+[[package]]
+name = "alloy-eips"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5591581ca2ab0b3e7226a4047f9a1bfcf431da1d0cce3752fda609fea3c27e37"
+dependencies = [
+ "alloy-eip2124",
+ "alloy-eip2930",
+ "alloy-eip7702",
+ "alloy-primitives",
+ "alloy-rlp",
+ "alloy-serde",
+ "auto_impl",
+ "c-kzg",
+ "derive_more",
+ "once_cell",
+ "serde",
+ "sha2",
+]
+
+[[package]]
+name = "alloy-json-rpc"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "762414662d793d7aaa36ee3af6928b6be23227df1681ce9c039f6f11daadef64"
+dependencies = [
+ "alloy-primitives",
+ "alloy-sol-types",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tracing",
+]
+
+[[package]]
+name = "alloy-network"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be03f2ebc00cf88bd06d3c6caf387dceaa9c7e6b268216779fa68a9bf8ab4e6"
+dependencies = [
+ "alloy-consensus",
+ "alloy-consensus-any",
+ "alloy-eips",
+ "alloy-json-rpc",
+ "alloy-network-primitives",
+ "alloy-primitives",
+ "alloy-rpc-types-any",
+ "alloy-rpc-types-eth",
+ "alloy-serde",
+ "alloy-signer",
+ "alloy-sol-types",
+ "async-trait",
+ "auto_impl",
+ "futures-utils-wasm",
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
+[[package]]
+name = "alloy-network-primitives"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a00ce618ae2f78369918be0c20f620336381502c83b6ed62c2f7b2db27698b0"
+dependencies = [
+ "alloy-consensus",
+ "alloy-eips",
+ "alloy-primitives",
+ "alloy-serde",
+ "serde",
+]
+
 [[package]]
 name = "alloy-primitives"
 version = "0.8.20"
@@ -26,6 +164,7 @@ dependencies = [
  "derive_more",
  "foldhash",
  "getrandom 0.2.15",
+ "hashbrown 0.15.2",
  "indexmap",
  "itoa",
  "k256",
@@ -47,10 +186,95 @@ version = "0.3.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d6c1d995bff8d011f7cd6c81820d51825e6e06d6db73914c1630ecf544d83d6"
 dependencies = [
+ "alloy-rlp-derive",
  "arrayvec",
  "bytes",
 ]
 
+[[package]]
+name = "alloy-rlp-derive"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a40e1ef334153322fd878d07e86af7a529bcb86b2439525920a88eba87bcf943"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
+[[package]]
+name = "alloy-rpc-types-any"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "318ae46dd12456df42527c3b94c1ae9001e1ceb707f7afe2c7807ac4e49ebad9"
+dependencies = [
+ "alloy-consensus-any",
+ "alloy-rpc-types-eth",
+ "alloy-serde",
+]
+
+[[package]]
+name = "alloy-rpc-types-eth"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b4dbee4d82f8a22dde18c28257bed759afeae7ba73da4a1479a039fd1445d04"
+dependencies = [
+ "alloy-consensus",
+ "alloy-consensus-any",
+ "alloy-eips",
+ "alloy-network-primitives",
+ "alloy-primitives",
+ "alloy-rlp",
+ "alloy-serde",
+ "alloy-sol-types",
+ "itertools 0.14.0",
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
+[[package]]
+name = "alloy-serde"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8732058f5ca28c1d53d241e8504620b997ef670315d7c8afab856b3e3b80d945"
+dependencies = [
+ "alloy-primitives",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "alloy-signer"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f96b3526fdd779a4bd0f37319cfb4172db52a7ac24cdbb8804b72091c18e1701"
+dependencies = [
+ "alloy-primitives",
+ "async-trait",
+ "auto_impl",
+ "either",
+ "elliptic-curve",
+ "k256",
+ "thiserror",
+]
+
+[[package]]
+name = "alloy-signer-local"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe8f78cd6b7501c7e813a1eb4a087b72d23af51f5bb66d4e948dc840bdd207d8"
+dependencies = [
+ "alloy-consensus",
+ "alloy-network",
+ "alloy-primitives",
+ "alloy-signer",
+ "async-trait",
+ "k256",
+ "rand",
+ "thiserror",
+]
+
 [[package]]
 name = "alloy-sol-macro"
 version = "0.8.22"
@@ -109,6 +333,22 @@ dependencies = [
  "const-hex",
 ]
 
+[[package]]
+name = "alloy-trie"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d95a94854e420f07e962f7807485856cde359ab99ab6413883e15235ad996e8b"
+dependencies = [
+ "alloy-primitives",
+ "alloy-rlp",
+ "arrayvec",
+ "derive_more",
+ "nybbles",
+ "serde",
+ "smallvec",
+ "tracing",
+]
+
 [[package]]
 name = "arbitrary"
 version = "1.4.1"
@@ -145,7 +385,7 @@ dependencies = [
  "ark-std 0.4.0",
  "derivative",
  "digest 0.10.7",
- "itertools",
+ "itertools 0.10.5",
  "num-bigint",
  "num-traits",
  "paste",
@@ -244,6 +484,20 @@ name = "arrayvec"
 version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
 
 [[package]]
 name = "auto_impl"
@@ -316,6 +570,18 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "blst"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47c79a94619fade3c0b887670333513a67ac28a6a7e653eb260bf0d4103db38d"
+dependencies = [
+ "cc",
+ "glob",
+ "threadpool",
+ "zeroize",
+]
+
 [[package]]
 name = "byte-slice-cast"
 version = "1.2.3"
@@ -333,6 +599,24 @@ name = "bytes"
 version = "1.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "c-kzg"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0307f72feab3300336fb803a57134159f6e20139af1357f36c54cb90d8e8928"
+dependencies = [
+ "blst",
+ "cc",
+ "glob",
+ "hex",
+ "libc",
+ "once_cell",
+ "serde",
+]
 
 [[package]]
 name = "cc"
@@ -406,6 +690,21 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "crc"
+version = "3.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
 [[package]]
 name = "crossbeam-utils"
 version = "0.8.21"
@@ -663,6 +962,12 @@ version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
 
+[[package]]
+name = "futures-utils-wasm"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9"
+
 [[package]]
 name = "generic-array"
 version = "0.14.7"
@@ -725,6 +1030,10 @@ name = "hashbrown"
 version = "0.15.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
+dependencies = [
+ "foldhash",
+ "serde",
+]
 
 [[package]]
 name = "heck"
@@ -732,11 +1041,20 @@ version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
 [[package]]
 name = "hex"
 version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "hmac"
@@ -776,6 +1094,7 @@ dependencies = [
  "arbitrary",
  "equivalent",
  "hashbrown 0.15.2",
+ "serde",
 ]
 
 [[package]]
@@ -787,6 +1106,15 @@ dependencies = [
  "either",
 ]
 
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
 [[package]]
 name = "itoa"
 version = "1.0.15"
@@ -876,9 +1204,11 @@ name = "motsu"
 version = "0.6.0"
 dependencies = [
  "alloy-primitives",
+ "alloy-signer-local",
  "alloy-sol-types",
  "const-hex",
  "dashmap",
+ "k256",
  "motsu-proc",
  "once_cell",
  "stylus-sdk",
@@ -927,6 +1257,27 @@ dependencies = [
  "libm",
 ]
 
+[[package]]
+name = "num_cpus"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "nybbles"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8983bb634df7248924ee0c4c3a749609b5abcb082c28fffe3254b3eb3602b307"
+dependencies = [
+ "const-hex",
+ "serde",
+ "smallvec",
+]
+
 [[package]]
 name = "once_cell"
 version = "1.21.0"
@@ -991,6 +1342,12 @@ dependencies = [
  "ucd-trie",
 ]
 
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
 [[package]]
 name = "pkcs8"
 version = "0.10.2"
@@ -1146,6 +1503,7 @@ dependencies = [
  "libc",
  "rand_chacha",
  "rand_core",
+ "serde",
 ]
 
 [[package]]
@@ -1474,6 +1832,9 @@ name = "smallvec"
 version = "1.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "spki"
@@ -1636,6 +1997,15 @@ dependencies = [
  "syn 2.0.100",
 ]
 
+[[package]]
+name = "threadpool"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
+dependencies = [
+ "num_cpus",
+]
+
 [[package]]
 name = "tiny-keccak"
 version = "2.0.2"
@@ -1679,6 +2049,37 @@ dependencies = [
  "winnow",
 ]
 
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
+dependencies = [
+ "once_cell",
+]
+
 [[package]]
 name = "trybuild"
 version = "1.0.104"
diff --git a/Cargo.toml b/Cargo.toml
index a763340..08e5b44 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -35,6 +35,8 @@ proc-macro2 = "1.0.79"
 quote = "1.0.35"
 alloy-primitives = { version = "=0.8.20", default-features = false }
 alloy-sol-types = { version = "=0.8.20", default-features = false }
+alloy-signer-local = { version = "=0.11.1", default-features = false }
+k256 = { version = "=0.13.4", default-features = false }
 
 # members
 motsu = { path = "crates/motsu" }
diff --git a/crates/motsu/Cargo.toml b/crates/motsu/Cargo.toml
index a5b1b6f..e9e1e70 100644
--- a/crates/motsu/Cargo.toml
+++ b/crates/motsu/Cargo.toml
@@ -17,6 +17,8 @@ motsu-proc.workspace = true
 dashmap.workspace = true
 alloy-primitives = { workspace = true, features = ["arbitrary", "rand"] }
 alloy-sol-types.workspace = true
+alloy-signer-local.workspace = true
+k256.workspace = true
 
 [lints]
 workspace = true
diff --git a/crates/motsu/README.md b/crates/motsu/README.md
index 2fdd008..14df714 100644
--- a/crates/motsu/README.md
+++ b/crates/motsu/README.md
@@ -9,9 +9,6 @@ Japanese -- we hold a stylus in our hand.
 
 ## Usage
 
-This crate enables unit-testing for Stylus contracts. It abstracts away the
-machinery necessary for writing tests behind a `#[motsu::test]` procedural macro.
-
 Annotate tests with `#[motsu::test]` instead of `#[test]`
 to get access to VM affordances:
 
@@ -24,7 +21,7 @@ mod tests {
     #[motsu::test]
     fn reads_balance(
         contract: Contract<Erc20>,
-        alice: Account,
+        alice: Address,
     ) {
         // Access storage.
         let balance = contract.sender(alice).balance_of(Address::ZERO);
@@ -33,22 +30,66 @@ mod tests {
 }
 ```
 
+If you need to instantiate an accound that contains a signer and a private key,
+you can use `Account` instead of `Address`:
+
+```rust
+#[cfg(test)]
+mod tests {
+    use motsu::prelude::*;
+    use alloy_signer::SignerSync;
+
+    #[motsu::test]
+    fn signs_message(alice: Account) {
+        let msg = "message".as_bytes();
+        let signer = alice.signer();
+        assert!(signer.sign_message_sync(msg).is_ok());
+    }
+}
+```
+
+### Global Variables
+
+Motsu allows you to manipulate certain global variables that affect the
+execution environment:
+
+#### Chain ID
+
+You can set the Chain ID in tests using the `VMContext` API:
+
+```rust,ignore
+use motsu::prelude::*;
+
+#[motsu::test]
+fn test_with_custom_chain_id(
+    contract: Contract<MyContract>,
+    alice: Address,
+) {
+    // Default chain ID is 42161 (Arbitrum One)
+
+    // Set chain ID to 11155111 (Sepolia testnet)
+    VMContext::current().set_chain_id(11155111);
+
+    // Now any contract code that depends on `block::chainid()` 
+    // will use this value
+}
+```
+
 ### Sender and Value
 
-Function `Contract::sender()` is necessary to trigger call
-to a contract, and should accept an `Account` or `Address` as an argument.
+Function `Contract::sender()` is necessary to trigger call to a contract, and
+should accept an `Account` or `Address` as an argument.
 
-Alternatively `Contract::sender_and_value()` can be used to
-pass additional value to the contract.
-To make a payable call work, user should be funded with
-`Account::fund` method (there is no funding by default),
-like in example below:
+Alternatively `Contract::sender_and_value()` can be used to pass additional
+value to the contract.
+To make a payable call work, user should be funded with `Funding::fund` method
+(each account has zero balance by default), like in example below:
 
 ```rust
 use motsu::prelude::*;
 
 #[motsu::test]
-fn pay_three_proxies(proxy: Contract<Proxy>, alice: Account) {
+fn pay_three_proxies(proxy: Contract<Proxy>, alice: Address) {
     let one = uint!(1_U256);
     let ten = uint!(10_U256);
 
@@ -83,7 +124,7 @@ fn call_three_proxies(
     proxy1: Contract<Proxy>,
     proxy2: Contract<Proxy>,
     proxy3: Contract<Proxy>,
-    alice: Account,
+    alice: Address,
 ) {
     let one = uint!(1_U256);
     let ten = uint!(10_U256);
diff --git a/crates/motsu/src/context.rs b/crates/motsu/src/context.rs
index 9ea0cb9..0a31d5c 100644
--- a/crates/motsu/src/context.rs
+++ b/crates/motsu/src/context.rs
@@ -11,13 +11,16 @@ use std::{
 };
 
 use alloy_primitives::{Address, Bytes, LogData, B256, U256};
+use alloy_signer_local::PrivateKeySigner;
 use alloy_sol_types::{SolEvent, Word};
 use dashmap::{mapref::one::RefMut, DashMap};
+use k256::ecdsa::SigningKey;
 use once_cell::sync::Lazy;
 use stylus_sdk::{
     host::{WasmVM, VM},
     keccak_const::Keccak256,
     prelude::StorageType,
+    types::AddressVM,
     ArbResult,
 };
 
@@ -39,10 +42,13 @@ use crate::{
 static MOTSU_VM: Lazy<DashMap<VMContext, VMContextStorage>> =
     Lazy::new(DashMap::new);
 
+// TODO: remove this after we can enable the `stylus-test` feature, which should
+// happen after we refactor `motsu` to implement a mock
+// `stylus_core::host::Host` trait.
 /// Arbitrum one chain id from [chain info].
 ///
 /// [chain info]: https://docs.arbitrum.io/for-devs/dev-tools-and-resources/chain-info
-pub const DEFAULT_CHAIN_ID: u64 = 42161;
+pub(crate) const DEFAULT_CHAIN_ID: u64 = 42161;
 
 /// Context of Motsu test VM associated with the current test thread.
 #[allow(clippy::module_name_repetitions)]
@@ -521,6 +527,17 @@ impl VMContext {
         MOTSU_VM.entry(self).or_default().tags.insert(address, tag);
     }
 
+    /// Get tag at `address`.
+    #[cfg(test)]
+    pub(crate) fn get_tag(self, address: Address) -> Option<String> {
+        MOTSU_VM
+            .entry(self)
+            .or_default()
+            .tags
+            .get_key_value(&address)
+            .map(|kv| kv.1.clone())
+    }
+
     /// Replaces non-checksumed addresses in the `msg` with corresponding tags
     /// (if any).
     pub(crate) fn replace_with_tags(self, mut msg: String) -> String {
@@ -922,10 +939,12 @@ pub(crate) fn create_default_storage_type<ST: StorageType>() -> ST {
     unsafe { ST::new(U256::ZERO, 0, VM(WasmVM {})) }
 }
 
-/// Account used to call contracts.
+/// Account that can be used to interact with contracts in test environments.
+/// Used to interact with and sign transactions for contracts.
 #[derive(Clone, Copy)]
 pub struct Account {
     address: Address,
+    private_key: B256,
 }
 
 impl From<Account> for Address {
@@ -935,16 +954,33 @@ impl From<Account> for Address {
 }
 
 impl Account {
-    /// Create a new account with the given `address`.
+    /// Creates an account with an address derived from the provided seed
+    /// string.
+    ///
+    /// The seed string is hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
     #[must_use]
-    pub const fn new_at(address: Address) -> Self {
-        Self { address }
+    pub fn from_seed(seed: &str) -> Self {
+        Self::from_seed_slice(seed.as_bytes())
     }
 
-    /// Create a new account with random address.
+    /// Creates an account with an address derived from the provided seed bytes.
+    ///
+    /// The seed bytes are hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    #[allow(clippy::missing_panics_doc)]
+    #[must_use]
+    pub fn from_seed_slice(seed: &[u8]) -> Self {
+        let private_key_bytes = Keccak256::new().update(seed).finalize();
+        let address = create_signer(&private_key_bytes).address();
+        let private_key = private_key_bytes.into();
+        Self { address, private_key }
+    }
+
+    /// Creates a new account with a randomly generated private key and address.
     #[must_use]
     pub fn random() -> Self {
-        Self::new_at(Address::random())
+        Self::from_seed_slice(B256::random().as_slice())
     }
 
     /// Get account's address.
@@ -952,54 +988,90 @@ impl Account {
     pub fn address(&self) -> Address {
         self.address
     }
+
+    /// Returns a signer that can be used to sign messages and transactions.
+    #[must_use]
+    pub fn signer(&self) -> PrivateKeySigner {
+        create_signer(self.private_key.as_slice())
+    }
+}
+
+/// Utility wrapper function for instantiating [`PrivateKeySigner`].
+fn create_signer(private_key: &[u8]) -> PrivateKeySigner {
+    PrivateKeySigner::from_signing_key(
+        SigningKey::from_slice(private_key)
+            .expect("failed to create signing key"),
+    )
 }
 
-/// Fund the account.
+/// Allows funding account with the chain's native token.
 pub trait Funding {
-    /// Fund the account with the given `value`.
+    /// Fund the account with the specified amount of the chain's native token.
     fn fund(&self, value: U256);
-
-    /// Get the balance of the account.
-    fn balance(&self) -> U256;
 }
 
 impl Funding for Address {
     fn fund(&self, value: U256) {
         VMContext::current().add_assign_balance(*self, value);
     }
-
-    fn balance(&self) -> U256 {
-        VMContext::current().balance(*self)
-    }
 }
 
 impl Funding for Account {
     fn fund(&self, value: U256) {
         self.address().fund(value);
     }
-
-    fn balance(&self) -> U256 {
-        self.address().balance()
-    }
 }
 
 impl<ST: StorageType + VMRouter + 'static> Funding for Contract<ST> {
     fn fund(&self, value: U256) {
         self.address().fund(value);
     }
+}
+
+/// Allows getting account's balance of chain's native token.
+pub trait Balance {
+    /// Get the chain's native token balance of the account.
+    fn balance(&self) -> U256;
+}
+
+impl Balance for Account {
+    fn balance(&self) -> U256 {
+        self.address().balance()
+    }
+}
 
+impl<ST: StorageType + VMRouter + 'static> Balance for Contract<ST> {
     fn balance(&self) -> U256 {
         self.address().balance()
     }
 }
 
-/// Deterministically derive from a string representation.
+/// Allows creating entities deterministically from a string identifier.
+///
+/// This trait enables consistent generation of addresses and contracts across
+/// test runs using meaningful string identifiers (tags).
 pub trait FromTag {
-    /// Deterministically derive inner address of `Self` from a string `tag`.
+    /// Creates an instance deterministically derived from the provided tag.
+    ///
+    /// The same tag will always produce the same entity.
     fn from_tag(tag: &str) -> Self;
 }
 
+impl FromTag for Account {
+    /// Creates an account derived from the tag string.
+    ///
+    /// Also registers the tag in the test context for debugging purposes.
+    fn from_tag(tag: &str) -> Self {
+        let account = Account::from_seed(tag);
+        VMContext::current().set_tag(account.address(), tag.to_string());
+        account
+    }
+}
+
 impl FromTag for Address {
+    /// Creates an Ethereum address derived from the tag string.
+    ///
+    /// Also registers the tag in the test context for debugging purposes.
     fn from_tag(tag: &str) -> Self {
         let hash = Keccak256::new().update(tag.as_bytes()).finalize();
         let address = Address::from_slice(&hash[..20]);
@@ -1008,14 +1080,124 @@ impl FromTag for Address {
     }
 }
 
-impl FromTag for Account {
+impl<ST: StorageType + VMRouter + 'static> FromTag for Contract<ST> {
+    /// Creates a contract at an address derived from the tag string.
+    ///
+    /// This allows deploying contracts to deterministic addresses for testing.
+    /// Also registers the tag in the test context for debugging purposes.
     fn from_tag(tag: &str) -> Self {
-        Account::new_at(Address::from_tag(tag))
+        Contract::new_at(Address::from_tag(tag))
     }
 }
 
-impl<ST: StorageType + VMRouter + 'static> FromTag for Contract<ST> {
-    fn from_tag(tag: &str) -> Self {
-        Contract::new_at(Address::from_tag(tag))
+#[cfg(test)]
+mod tests {
+    use alloy_primitives::{address, b256};
+    use stylus_sdk::prelude::*;
+
+    use super::{Account, Address, Contract, FromTag};
+    use crate::context::VMContext;
+
+    mod account {
+        use super::*;
+
+        #[test]
+        fn from_seed() {
+            let seed = "some seed";
+            let expected_private_key = b256!("f5bab94a7fcf9b243fc4b28b4e2011a196e6c86286297b5e8d5f157ecd0f9d31");
+            let expected_address =
+                address!("0x94cf44a0c23e70feee6c1fdbaebe7dc6f1172c6d");
+
+            let account = Account::from_seed(&seed);
+            assert_eq!(expected_private_key, account.private_key);
+            assert_eq!(expected_address, account.address());
+
+            // verify signer can be recreated
+            let signer = account.signer();
+            assert_eq!(account.address(), signer.address());
+        }
+
+        #[test]
+        fn from_seed_bytes() {
+            let seed = [115, 111, 109, 101, 32, 115, 101, 101, 100];
+            let expected_private_key = b256!("f5bab94a7fcf9b243fc4b28b4e2011a196e6c86286297b5e8d5f157ecd0f9d31");
+            let expected_address =
+                address!("0x94cf44a0c23e70feee6c1fdbaebe7dc6f1172c6d");
+
+            let account = Account::from_seed_slice(&seed);
+            assert_eq!(expected_private_key, account.private_key);
+            assert_eq!(expected_address, account.address());
+
+            // verify signer can be recreated
+            let signer = account.signer();
+            assert_eq!(account.address(), signer.address());
+        }
+
+        #[test]
+        fn random() {
+            let account = Account::random();
+            assert!(!account.private_key.is_zero());
+            assert!(!account.address().is_zero());
+
+            // verify signer can be recreated
+            let signer = account.signer();
+            assert_eq!(account.address(), signer.address());
+        }
+    }
+
+    mod from_tag {
+        use super::*;
+
+        #[test]
+        fn account() {
+            let tag = String::from("signer");
+            let expected_private_key = b256!("6c8d7f768a6bb4aafe85e8a2f5a9680355239c7e14646ed62b044e39de154512");
+            let expected_address =
+                address!("0x6e12d8c87503d4287c294f2fdef96acd9dff6bd2");
+
+            let account = Account::from_tag(&tag);
+            assert_eq!(expected_private_key, account.private_key);
+            assert_eq!(expected_address, account.address());
+
+            assert_eq!(
+                Some(tag),
+                VMContext::current().get_tag(account.address())
+            );
+        }
+
+        #[test]
+        fn address() {
+            let tag = String::from("alice");
+            let expected_address =
+                address!("0x9c0257114eb9399a2985f8e75dad7600c5d89fe3");
+
+            let address = Address::from_tag(&tag);
+            assert_eq!(expected_address, address);
+
+            assert_eq!(Some(tag), VMContext::current().get_tag(address));
+        }
+
+        #[storage]
+        struct SomeContract;
+
+        #[public]
+        impl SomeContract {}
+
+        unsafe impl TopLevelStorage for SomeContract {}
+
+        #[test]
+        fn contract() {
+            let tag = String::from("contract");
+            let expected_address =
+                address!("0x7f6dd79f0020bee2024a097aaa5d32ab7ca31126");
+
+            let contract = Contract::<SomeContract>::from_tag(&tag);
+            assert_eq!(expected_address, contract.address());
+
+            assert_eq!(
+                Some(tag),
+                VMContext::current().get_tag(contract.address())
+            );
+        }
     }
 }
diff --git a/crates/motsu/src/lib.rs b/crates/motsu/src/lib.rs
index 819d40a..0121a8e 100644
--- a/crates/motsu/src/lib.rs
+++ b/crates/motsu/src/lib.rs
@@ -21,7 +21,7 @@
 //!     #[motsu::test]
 //!     fn reads_balance(
 //!         contract: Contract<Erc20>,
-//!         alice: Account,
+//!         alice: Address,
 //!     ) {
 //!         // Access storage.
 //!         let balance = contract.sender(alice).balance_of(Address::ZERO);
@@ -30,6 +30,25 @@
 //! }
 //! ```
 //!
+//! If you need to instantiate an accound that contains a signer and a private
+//! key, you can use [`crate::prelude::Account`] instead of
+//! [`stylus_sdk::alloy_primitives::Address`]:
+//!
+//! ```rust,ignore
+//! #[cfg(test)]
+//! mod tests {
+//!     use motsu::prelude::*;
+//!     use alloy_signer::SignerSync;
+//!
+//!     #[motsu::test]
+//!     fn signs_message(alice: Account) {
+//!         let msg = "message".as_bytes();
+//!         let signer = alice.signer();
+//!         assert!(signer.sign_message_sync(msg).is_ok());
+//!     }
+//! }
+//! ```
+//!
 //! ### Global Variables
 //!
 //! Motsu allows you to manipulate certain global variables that affect the
@@ -45,7 +64,7 @@
 //! #[motsu::test]
 //! fn test_with_custom_chain_id(
 //!     contract: Contract<MyContract>,
-//!     alice: Account,
+//!     alice: Address,
 //! ) {
 //!     // Default chain ID is 42161 (Arbitrum One)
 //!
@@ -74,7 +93,7 @@
 //!  use stylus_sdk::alloy_primitives::{Address, U256, ruint::uint};
 //!
 //!  #[motsu::test]
-//!  fn pay_three_proxies(proxy: Contract<Proxy>, alice: Account) {
+//!  fn pay_three_proxies(proxy: Contract<Proxy>, alice: Address) {
 //!     let one = uint!(1_U256);
 //!     let ten = uint!(10_U256);
 //!
@@ -96,9 +115,9 @@
 //! ### External Calls
 //!
 //! Multiple external calls are supported in Motsu.
-//! Assuming `Proxy` is a contract that exposes `#[public]` function
-//! `Proxy::call_proxy`, where it adds `one` to the passed argument and calls
-//! next `Proxy` contract at the address provided during initialization.
+//! Assuming `Proxy` is a contract that exposes [`stylus_sdk::prelude::public`]
+//! function `Proxy::call_proxy`, where it adds `one` to the passed argument and
+//! calls next `Proxy` contract at the address provided during initialization.
 //! The following test case can emulate a call chain of three `Proxy` contracts:
 //!
 //! ```rust,ignore
@@ -110,7 +129,7 @@
 //!     proxy1: Contract<Proxy>,
 //!     proxy2: Contract<Proxy>,
 //!     proxy3: Contract<Proxy>,
-//!     alice: Account,
+//!     alice: Address,
 //!  ) {
 //!     let one = uint!(1_U256);
 //!     let ten = uint!(10_U256);
@@ -380,7 +399,7 @@ mod ping_pong_tests {
     fn external_call(
         ping: Contract<PingContract>,
         pong: Contract<PongContract>,
-        alice: Account,
+        alice: Address,
     ) {
         let value = TEN;
         let ponged_value =
@@ -395,7 +414,7 @@ mod ping_pong_tests {
     fn external_call_error(
         ping: Contract<PingContract>,
         pong: Contract<PongContract>,
-        alice: Account,
+        alice: Address,
     ) {
         let value = MAGIC_ERROR_VALUE;
         let err =
@@ -411,7 +430,7 @@ mod ping_pong_tests {
     fn external_call_panic(
         ping: Contract<PingContract>,
         pong: Contract<PongContract>,
-        alice: Account,
+        alice: Address,
     ) {
         let value = MAGIC_ERROR_VALUE;
         ping.sender(alice).ping(pong.address(), value).motsu_unwrap();
@@ -421,7 +440,7 @@ mod ping_pong_tests {
     fn external_static_call(
         ping: Contract<PingContract>,
         pong: Contract<PongContract>,
-        alice: Account,
+        alice: Address,
     ) {
         let can_ping =
             ping.sender(alice).can_ping(pong.address()).motsu_unwrap();
@@ -432,14 +451,14 @@ mod ping_pong_tests {
     fn msg_sender(
         ping: Contract<PingContract>,
         pong: Contract<PongContract>,
-        alice: Account,
+        alice: Address,
     ) {
         assert_eq!(ping.sender(alice).pinged_from.get(), Address::ZERO);
         assert_eq!(pong.sender(alice).ponged_from.get(), Address::ZERO);
 
         ping.sender(alice).ping(pong.address(), TEN).motsu_unwrap();
 
-        assert_eq!(ping.sender(alice).pinged_from.get(), alice.address());
+        assert_eq!(ping.sender(alice).pinged_from.get(), alice);
         assert_eq!(pong.sender(alice).ponged_from.get(), ping.address());
     }
 
@@ -447,7 +466,7 @@ mod ping_pong_tests {
     fn has_code(
         ping: Contract<PingContract>,
         pong: Contract<PongContract>,
-        alice: Account,
+        alice: Address,
     ) {
         assert!(ping.sender(alice).has_pong(pong.address()));
     }
@@ -456,7 +475,7 @@ mod ping_pong_tests {
     fn contract_address(
         ping: Contract<PingContract>,
         pong: Contract<PongContract>,
-        alice: Account,
+        alice: Address,
     ) {
         assert_eq!(ping.sender(alice).contract_address.get(), Address::ZERO);
         assert_eq!(pong.sender(alice).contract_address.get(), Address::ZERO);
@@ -471,7 +490,7 @@ mod ping_pong_tests {
     fn deref_invalidated_storage_cache(
         ping: Contract<PingContract>,
         pong: Contract<PongContract>,
-        alice: Account,
+        alice: Address,
     ) {
         // This test is different from `contract_address` test, since it checks
         // that `StorageType` contract correctly resets.
@@ -508,13 +527,13 @@ mod ping_pong_tests {
     // chain, this is still an allowed "feature" of motsu, so we "document" the
     // behavior with this unit test.
     #[motsu::test]
-    fn storage_cleanup(alice: Account, addr: Address) {
+    fn storage_cleanup(alice: Address, addr: Address) {
         // First contract
         let pong1 = Contract::<PongContract>::new_at(addr);
 
         pong1.sender(alice).pong(U256::ZERO).expect("should pong");
 
-        assert_eq!(alice.address(), pong1.sender(alice).ponged_from.get());
+        assert_eq!(alice, pong1.sender(alice).ponged_from.get());
         assert_eq!(ONE, pong1.sender(alice).pongs_count.get());
         assert_eq!(addr, pong1.sender(alice).contract_address.get());
 
@@ -534,20 +553,18 @@ mod ping_pong_tests {
     fn emits_event(
         ping: Contract<PingContract>,
         pong: Contract<PongContract>,
-        alice: Account,
+        alice: Address,
     ) {
         ping.sender(alice).ping(pong.address(), TEN).motsu_unwrap();
 
         // Assert emitted events.
-        ping.assert_emitted(&Pinged { from: alice.address(), value: TEN });
+        ping.assert_emitted(&Pinged { from: alice, value: TEN });
         pong.assert_emitted(&Ponged { from: ping.address(), value: TEN });
 
         // Assert not emitted events
         assert!(!ping.emitted(&Pinged { from: ping.address(), value: TEN }));
-        assert!(!pong.emitted(&Ponged { from: alice.address(), value: TEN }));
-        assert!(
-            !ping.emitted(&Pinged { from: alice.address(), value: TEN + ONE })
-        );
+        assert!(!pong.emitted(&Ponged { from: alice, value: TEN }));
+        assert!(!ping.emitted(&Pinged { from: alice, value: TEN + ONE }));
         assert!(
             !pong.emitted(&Ponged { from: ping.address(), value: TEN + ONE })
         );
@@ -560,7 +577,7 @@ mod ping_pong_tests {
     fn asserted_emitted_event(
         ping: Contract<PingContract>,
         pong: Contract<PongContract>,
-        alice: Account,
+        alice: Address,
     ) {
         ping.sender(alice).ping(pong.address(), TEN).motsu_unwrap();
 
@@ -576,17 +593,17 @@ mod ping_pong_tests {
     fn assert_reverts_emitted_event(
         ping: Contract<PingContract>,
         pong: Contract<PongContract>,
-        alice: Account,
+        alice: Address,
     ) {
         let value = MAGIC_ERROR_VALUE;
         ping.sender(alice).ping(pong.address(), value).motsu_unwrap_err();
 
         // Both events should not be emitted after revert.
-        assert!(!ping.emitted(&Pinged { from: alice.address(), value }));
+        assert!(!ping.emitted(&Pinged { from: alice, value }));
         assert!(!pong.emitted(&Ponged { from: ping.address(), value }));
 
         // Check panic assertion.
-        ping.assert_emitted(&Pinged { from: alice.address(), value });
+        ping.assert_emitted(&Pinged { from: alice, value });
     }
 }
 
@@ -836,7 +853,7 @@ mod proxies_tests {
         proxy1: Contract<Proxy>,
         proxy2: Contract<Proxy>,
         proxy3: Contract<Proxy>,
-        alice: Account,
+        alice: Address,
     ) {
         // Set up a chain of three proxies.
         // With the given call chain: proxy1 -> proxy2 -> proxy3.
@@ -856,7 +873,7 @@ mod proxies_tests {
         proxy1: Contract<Proxy>,
         proxy2: Contract<Proxy>,
         proxy3: Contract<Proxy>,
-        alice: Account,
+        alice: Address,
     ) {
         // Set up a chain of three proxies.
         // With the given call chain: proxy1 -> proxy2 -> proxy3 -> proxy1 -> ..
@@ -876,7 +893,7 @@ mod proxies_tests {
         proxy1: Contract<Proxy>,
         proxy2: Contract<Proxy>,
         proxy3: Contract<Proxy>,
-        alice: Account,
+        alice: Address,
     ) {
         // Set up a chain of three proxies.
         // With the given call chain: proxy1 -> proxy2 -> proxy3.
@@ -905,7 +922,7 @@ mod proxies_tests {
         proxy1: Contract<Proxy>,
         proxy2: Contract<Proxy>,
         proxy3: Contract<Proxy>,
-        alice: Account,
+        alice: Address,
     ) {
         // Set up a chain of three proxies.
         // With the given call chain: proxy1 -> proxy2 -> proxy3.
@@ -935,7 +952,7 @@ mod proxies_tests {
         proxy1: Contract<Proxy>,
         proxy2: Contract<Proxy>,
         proxy3: Contract<Proxy>,
-        alice: Account,
+        alice: Address,
     ) {
         // Set up a chain of three proxies.
         // With the given call chain: proxy1 -> proxy2 -> proxy3.
@@ -968,7 +985,7 @@ mod proxies_tests {
             let proxy1 = Contract::<Proxy>::new();
             let proxy2 = Contract::<Proxy>::new();
             let proxy3 = Contract::<Proxy>::new();
-            let alice = Account::random();
+            let alice = Address::random();
 
             // Set up a chain of three proxies.
             // With the given call chain: proxy1 -> proxy2 -> proxy3.
@@ -990,7 +1007,7 @@ mod proxies_tests {
         proxy2: Contract<Proxy>,
         proxy3: Contract<Proxy>,
         proxy4: Contract<Proxy>,
-        alice: Account,
+        alice: Address,
     ) {
         // Set up a chain of four proxies.
         // With the given call chain: proxy1 -> proxy2 -> proxy3 -> proxy4.
@@ -1036,7 +1053,7 @@ mod proxies_tests {
         proxy2: Contract<Proxy>,
         proxy3: Contract<Proxy>,
         proxy4: Contract<Proxy>,
-        alice: Account,
+        alice: Address,
     ) {
         // Set up a chain of four proxies.
         // With the given call chain: proxy1 -> proxy2 -> proxy3 -> proxy4.
@@ -1087,7 +1104,7 @@ mod proxies_tests {
         proxy2: Contract<Proxy>,
         proxy3: Contract<Proxy>,
         proxy4: Contract<Proxy>,
-        alice: Account,
+        alice: Address,
     ) {
         // Set up a chain of four proxies.
         // With the given call chain: proxy1 -> proxy2 -> proxy3 -> proxy4.
@@ -1130,10 +1147,10 @@ mod proxies_tests {
 
 #[cfg(test)]
 mod vm_tests {
-    use stylus_sdk::{block, prelude::*};
+    use stylus_sdk::{alloy_primitives::Address, block, prelude::*};
 
     use crate as motsu;
-    use crate::prelude::*;
+    use crate::{context::DEFAULT_CHAIN_ID, prelude::*};
 
     const ETHEREUM_SEPOLIA_CHAIN_ID: u64 = 11155111;
     const CUSTOM_CHAIN_ID: u64 = 12345678987654321;
@@ -1151,7 +1168,7 @@ mod vm_tests {
     unsafe impl TopLevelStorage for ChainChecker {}
 
     #[motsu::test]
-    fn chain_id(contract: Contract<ChainChecker>, alice: Account) {
+    fn chain_id(contract: Contract<ChainChecker>, alice: Address) {
         // Default chain ID is Arbitrum One
         let chain_id = contract.sender(alice).get_chain_id();
         assert_eq!(chain_id, DEFAULT_CHAIN_ID);
diff --git a/crates/motsu/src/prelude.rs b/crates/motsu/src/prelude.rs
index a377702..f3f8727 100644
--- a/crates/motsu/src/prelude.rs
+++ b/crates/motsu/src/prelude.rs
@@ -1,8 +1,7 @@
 //! Common imports for `motsu` tests.
 pub use crate::{
     context::{
-        Account, Contract, ContractCall, FromTag, Funding, VMContext,
-        DEFAULT_CHAIN_ID,
+        Account, Balance, Contract, ContractCall, FromTag, Funding, VMContext,
     },
     revert::ResultExt,
 };


Reviews:
user: login: qalisander
body: Good job!
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T10:08:53Z
user: login: 0xNeshi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T11:32:49Z
user: login: qalisander
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T11:50:11Z
user: login: 0xNeshi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T11:55:35Z
user: login: 0xNeshi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T12:02:55Z
user: login: qalisander
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T12:07:51Z
user: login: 0xNeshi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T12:13:21Z
user: login: 0xNeshi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T12:17:04Z
user: login: qalisander
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T14:50:29Z
user: login: qalisander
body: Looking good!
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T15:13:14Z
user: login: 0xNeshi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T15:54:16Z
user: login: 0xNeshi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T16:00:10Z
user: login: 0xNeshi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T16:04:26Z
user: login: qalisander
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T16:12:10Z
user: login: qalisander
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T16:13:23Z
user: login: qalisander
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T16:15:45Z
user: login: qalisander
body: Good job!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-14T16:33:24Z
user: login: qalisander
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T16:43:09Z
user: login: 0xNeshi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T07:34:53Z
user: login: 0xNeshi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T07:39:05Z
user: login: 0xNeshi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T07:42:25Z

Review comments:
diff_hunk: @@ -903,23 +918,75 @@ impl From<Account> for Address {
 }
 
 impl Account {
-    /// Create a new account with the given `address`.
+    /// Creates an account with an address derived from the provided seed
+    /// string.
+    ///
+    /// The seed string is hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    ///
+    /// # Panics
+    ///
+    /// * If the signing key creation fails with the keccak hash of the seed
+    ///   (shouldn't happen).
path: crates/motsu/src/context.rs
user: login: qalisander
body: If it shouldn't happen we shouldn't write this panic in docs, and in the other function also
created_at: 2025-03-13T09:23:26Z
author_association: MEMBER
diff_hunk: @@ -903,23 +918,75 @@ impl From<Account> for Address {
 }
 
 impl Account {
-    /// Create a new account with the given `address`.
+    /// Creates an account with an address derived from the provided seed
+    /// string.
+    ///
+    /// The seed string is hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    ///
+    /// # Panics
+    ///
+    /// * If the signing key creation fails with the keccak hash of the seed
+    ///   (shouldn't happen).
     #[must_use]
-    pub const fn new_at(address: Address) -> Self {
-        Self { address }
+    pub fn from_seed(seed: &str) -> Self {
+        Self::from_seed_bytes(seed.as_bytes())
     }
 
-    /// Create a new account with random address.
+    /// Creates an account with an address derived from the provided seed bytes.
+    ///
+    /// The seed bytes are hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    ///
+    /// # Panics
+    ///
+    /// * If the signing key creation fails with the keccak hash of the seed
+    ///   (shouldn't happen).
+    #[must_use]
+    pub fn from_seed_bytes(seed_bytes: &[u8]) -> Self {
path: crates/motsu/src/context.rs
user: login: qalisander
body: Do we need `from_seed` and `from_seed_bytes` to be public? There is a reason we're not allowing to set `msg_sender` outside of `.sender(..)` function. Because users don't need to use it and we will have a public api less confusing and less items in autocomplete
created_at: 2025-03-13T09:27:23Z
author_association: MEMBER
diff_hunk: @@ -890,10 +903,12 @@ pub(crate) fn create_default_storage_type<ST: StorageType>() -> ST {
     unsafe { ST::new(U256::ZERO, 0, VM { host: Box::new(WasmVM {}) }) }
 }
 
-/// Account used to call contracts.
+/// Account that can be used to interact with contracts in test environments.
+/// Used to interact with and sign transactions for contracts.
path: crates/motsu/src/context.rs
user: login: qalisander
body: Good description!
created_at: 2025-03-13T09:29:06Z
author_association: MEMBER
diff_hunk: @@ -961,29 +1028,136 @@ impl<ST: StorageType + VMRouter + 'static> Funding for Contract<ST> {
     }
 }
 
-/// Deterministically derive from a string representation.
+/// Allows creating entities deterministically from a string identifier.
+///
+/// This trait enables consistent generation of addresses and contracts across
+/// test runs using meaningful string identifiers (tags).
 pub trait FromTag {
-    /// Deterministically derive inner address of `Self` from a string `tag`.
+    /// Creates an instance deterministically derived from the provided tag.
+    ///
+    /// The same tag will always produce the same entity.
     fn from_tag(tag: &str) -> Self;
 }
 
-impl FromTag for Address {
+impl FromTag for Account {
+    /// Creates an account derived from the tag string.
+    ///
+    /// Also registers the tag in the test context for debugging purposes.
     fn from_tag(tag: &str) -> Self {
-        let hash = Keccak256::new().update(tag.as_bytes()).finalize();
-        let address = Address::from_slice(&hash[..20]);
-        VMContext::current().set_tag(address, tag.to_string());
-        address
+        let account = Account::from_seed(tag);
+        VMContext::current().set_tag(account.address(), tag.to_string());
+        account
     }
 }
 
-impl FromTag for Account {
+impl FromTag for Address {
+    /// Creates an Ethereum address derived from the tag string.
+    ///
+    /// Also registers the tag in the test context for debugging purposes.
     fn from_tag(tag: &str) -> Self {
-        Account::new_at(Address::from_tag(tag))
+        Account::from_tag(tag).address()
     }
 }
 
 impl<ST: StorageType + VMRouter + 'static> FromTag for Contract<ST> {
+    /// Creates a contract at an address derived from the tag string.
+    ///
+    /// This allows deploying contracts to deterministic addresses for testing.
+    /// Also registers the tag in the test context for debugging purposes.
     fn from_tag(tag: &str) -> Self {
         Contract::new_at(Address::from_tag(tag))
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use alloy_primitives::private::proptest::proptest;
+    use stylus_sdk::prelude::*;
+
+    use super::{Account, Address, Contract, FromTag};
+    use crate::context::VMContext;
+
+    mod account {
+        use super::*;
+
+        #[test]
+        fn account_from_seed() {
+            proptest!(|(seed: String)| {
+                let account = Account::from_seed(&seed);
+                assert!(!account.private_key.is_zero());
+                assert!(!account.address().is_zero());
+
+                // verify signer can be recreated
+                let signer = account.signer();
+                assert_eq!(account.address(), signer.address());
+            })
+        }
+
+        #[test]
+        fn account_from_seed_bytes() {
+            proptest!(|(seed: Vec<u8>)| {
+                let account = Account::from_seed_bytes(&seed);
+                assert!(!account.private_key.is_zero());
+                assert!(!account.address().is_zero());
+
+                // verify signer can be recreated
+                let signer = account.signer();
+                assert_eq!(account.address(), signer.address());
+            })
+        }
+
+        #[test]
+        fn account_random() {
+            let account = Account::random();
+            assert!(!account.private_key.is_zero());
+            assert!(!account.address().is_zero());
+
+            // verify signer can be recreated
+            let signer = account.signer();
+            assert_eq!(account.address(), signer.address());
+        }
+    }
+
+    mod from_tag {
path: crates/motsu/src/context.rs
user: login: qalisander
body: I would say these are not the most important proptests to have. Tags are related to minor thing (pretty printing), logic is straightforward and you've added `get_tag` just to make these tests. It's more like creation of code just to have more tests.

Can't we check `replace_with_tags` instead (no necessary proptest)?
created_at: 2025-03-13T09:44:28Z
author_association: MEMBER
diff_hunk: @@ -961,29 +1028,136 @@ impl<ST: StorageType + VMRouter + 'static> Funding for Contract<ST> {
     }
 }
 
-/// Deterministically derive from a string representation.
+/// Allows creating entities deterministically from a string identifier.
+///
+/// This trait enables consistent generation of addresses and contracts across
+/// test runs using meaningful string identifiers (tags).
 pub trait FromTag {
-    /// Deterministically derive inner address of `Self` from a string `tag`.
+    /// Creates an instance deterministically derived from the provided tag.
+    ///
+    /// The same tag will always produce the same entity.
     fn from_tag(tag: &str) -> Self;
 }
 
-impl FromTag for Address {
+impl FromTag for Account {
+    /// Creates an account derived from the tag string.
+    ///
+    /// Also registers the tag in the test context for debugging purposes.
     fn from_tag(tag: &str) -> Self {
-        let hash = Keccak256::new().update(tag.as_bytes()).finalize();
-        let address = Address::from_slice(&hash[..20]);
-        VMContext::current().set_tag(address, tag.to_string());
-        address
+        let account = Account::from_seed(tag);
+        VMContext::current().set_tag(account.address(), tag.to_string());
+        account
     }
 }
 
-impl FromTag for Account {
+impl FromTag for Address {
+    /// Creates an Ethereum address derived from the tag string.
+    ///
+    /// Also registers the tag in the test context for debugging purposes.
     fn from_tag(tag: &str) -> Self {
-        Account::new_at(Address::from_tag(tag))
+        Account::from_tag(tag).address()
     }
 }
 
 impl<ST: StorageType + VMRouter + 'static> FromTag for Contract<ST> {
+    /// Creates a contract at an address derived from the tag string.
+    ///
+    /// This allows deploying contracts to deterministic addresses for testing.
+    /// Also registers the tag in the test context for debugging purposes.
     fn from_tag(tag: &str) -> Self {
         Contract::new_at(Address::from_tag(tag))
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use alloy_primitives::private::proptest::proptest;
+    use stylus_sdk::prelude::*;
+
+    use super::{Account, Address, Contract, FromTag};
+    use crate::context::VMContext;
+
+    mod account {
+        use super::*;
+
+        #[test]
+        fn account_from_seed() {
+            proptest!(|(seed: String)| {
+                let account = Account::from_seed(&seed);
+                assert!(!account.private_key.is_zero());
+                assert!(!account.address().is_zero());
+
+                // verify signer can be recreated
+                let signer = account.signer();
+                assert_eq!(account.address(), signer.address());
+            })
+        }
+
+        #[test]
+        fn account_from_seed_bytes() {
+            proptest!(|(seed: Vec<u8>)| {
+                let account = Account::from_seed_bytes(&seed);
+                assert!(!account.private_key.is_zero());
+                assert!(!account.address().is_zero());
+
+                // verify signer can be recreated
+                let signer = account.signer();
+                assert_eq!(account.address(), signer.address());
+            })
+        }
+
+        #[test]
+        fn account_random() {
+            let account = Account::random();
+            assert!(!account.private_key.is_zero());
+            assert!(!account.address().is_zero());
+
+            // verify signer can be recreated
+            let signer = account.signer();
+            assert_eq!(account.address(), signer.address());
+        }
+    }
+
+    mod from_tag {
+        use super::*;
+
+        #[test]
+        fn account() {
+            proptest!(|(tag: String)| {
+                let account = Account::from_tag(&tag);
+                assert!(!account.private_key.is_zero());
+                assert!(!account.address().is_zero());
+
+                assert_eq!(Some(tag), VMContext::current().get_tag(account.address()));
+            })
+        }
+
+        #[test]
+        fn address() {
+            proptest!(|(tag: String)| {
+                let address = Address::from_tag(&tag);
+                assert!(!address.is_zero());
+
+                assert_eq!(Some(tag), VMContext::current().get_tag(address));
+            })
+        }
+
+        #[storage]
+        struct SomeContract;
+
+        #[public]
+        impl SomeContract {}
+
+        unsafe impl TopLevelStorage for SomeContract {}
+
+        #[test]
+        fn contract() {
+            proptest!(|(tag: String)| {
+                let contract = Contract::<SomeContract>::from_tag(&tag);
+                assert!(!contract.address().is_zero());
+
+                assert_eq!(Some(tag), VMContext::current().get_tag(contract.address()));
+            })
+        }
+    }
path: crates/motsu/src/context.rs
user: login: qalisander
body: nit: And we're missing an actual test case where we're able to sign something and validate that signature corresponds to the internal account address.
May be we don't need that test since we rely mostly on alloy signer implementation, but this pr is all about it
created_at: 2025-03-13T09:59:55Z
author_association: MEMBER
diff_hunk: @@ -903,23 +918,75 @@ impl From<Account> for Address {
 }
 
 impl Account {
-    /// Create a new account with the given `address`.
+    /// Creates an account with an address derived from the provided seed
+    /// string.
+    ///
+    /// The seed string is hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    ///
+    /// # Panics
+    ///
+    /// * If the signing key creation fails with the keccak hash of the seed
+    ///   (shouldn't happen).
     #[must_use]
-    pub const fn new_at(address: Address) -> Self {
-        Self { address }
+    pub fn from_seed(seed: &str) -> Self {
+        Self::from_seed_bytes(seed.as_bytes())
     }
 
-    /// Create a new account with random address.
+    /// Creates an account with an address derived from the provided seed bytes.
+    ///
+    /// The seed bytes are hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    ///
+    /// # Panics
+    ///
+    /// * If the signing key creation fails with the keccak hash of the seed
+    ///   (shouldn't happen).
+    #[must_use]
+    pub fn from_seed_bytes(seed_bytes: &[u8]) -> Self {
path: crates/motsu/src/context.rs
user: login: 0xNeshi
body: `from_seed` is used to deterministically create `Account` from a given seed (see comment above: `The same seed will always produce the same account address`). It replaces `new_at`.
`from_seed_bytes` is the underlying function performing the actual work, but I chose to expose it as a utility for whoever may need it.
We cannot say for sure whether users need or don't need to use this, and since exposing either of these doesn't hurt us, while at the same time can help users, I think it's safe to do.

> less items in autocomplete

I think this is a weak argument for _not_ exposing a function, type `String::from` or `U256::from` in your editor and see how many functions pop up :smile: You usually know what you want to use, and if you don't you google it, so a long autocomplete list isn't a problem. And it's better to have many options for instantiating `Account`, as long as they're maintainable
created_at: 2025-03-13T11:32:49Z
author_association: COLLABORATOR
diff_hunk: @@ -903,23 +918,75 @@ impl From<Account> for Address {
 }
 
 impl Account {
-    /// Create a new account with the given `address`.
+    /// Creates an account with an address derived from the provided seed
+    /// string.
+    ///
+    /// The seed string is hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    ///
+    /// # Panics
+    ///
+    /// * If the signing key creation fails with the keccak hash of the seed
+    ///   (shouldn't happen).
     #[must_use]
-    pub const fn new_at(address: Address) -> Self {
-        Self { address }
+    pub fn from_seed(seed: &str) -> Self {
+        Self::from_seed_bytes(seed.as_bytes())
     }
 
-    /// Create a new account with random address.
+    /// Creates an account with an address derived from the provided seed bytes.
+    ///
+    /// The seed bytes are hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    ///
+    /// # Panics
+    ///
+    /// * If the signing key creation fails with the keccak hash of the seed
+    ///   (shouldn't happen).
+    #[must_use]
+    pub fn from_seed_bytes(seed_bytes: &[u8]) -> Self {
path: crates/motsu/src/context.rs
user: login: qalisander
body: > doesn't hurt us

Once we decide to expose a bit more than we need, it can be a subject to breaking changes in future. Like it already happened with `new_at`:D


created_at: 2025-03-13T11:50:11Z
author_association: MEMBER
diff_hunk: @@ -961,29 +1028,136 @@ impl<ST: StorageType + VMRouter + 'static> Funding for Contract<ST> {
     }
 }
 
-/// Deterministically derive from a string representation.
+/// Allows creating entities deterministically from a string identifier.
+///
+/// This trait enables consistent generation of addresses and contracts across
+/// test runs using meaningful string identifiers (tags).
 pub trait FromTag {
-    /// Deterministically derive inner address of `Self` from a string `tag`.
+    /// Creates an instance deterministically derived from the provided tag.
+    ///
+    /// The same tag will always produce the same entity.
     fn from_tag(tag: &str) -> Self;
 }
 
-impl FromTag for Address {
+impl FromTag for Account {
+    /// Creates an account derived from the tag string.
+    ///
+    /// Also registers the tag in the test context for debugging purposes.
     fn from_tag(tag: &str) -> Self {
-        let hash = Keccak256::new().update(tag.as_bytes()).finalize();
-        let address = Address::from_slice(&hash[..20]);
-        VMContext::current().set_tag(address, tag.to_string());
-        address
+        let account = Account::from_seed(tag);
+        VMContext::current().set_tag(account.address(), tag.to_string());
+        account
     }
 }
 
-impl FromTag for Account {
+impl FromTag for Address {
+    /// Creates an Ethereum address derived from the tag string.
+    ///
+    /// Also registers the tag in the test context for debugging purposes.
     fn from_tag(tag: &str) -> Self {
-        Account::new_at(Address::from_tag(tag))
+        Account::from_tag(tag).address()
     }
 }
 
 impl<ST: StorageType + VMRouter + 'static> FromTag for Contract<ST> {
+    /// Creates a contract at an address derived from the tag string.
+    ///
+    /// This allows deploying contracts to deterministic addresses for testing.
+    /// Also registers the tag in the test context for debugging purposes.
     fn from_tag(tag: &str) -> Self {
         Contract::new_at(Address::from_tag(tag))
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use alloy_primitives::private::proptest::proptest;
+    use stylus_sdk::prelude::*;
+
+    use super::{Account, Address, Contract, FromTag};
+    use crate::context::VMContext;
+
+    mod account {
+        use super::*;
+
+        #[test]
+        fn account_from_seed() {
+            proptest!(|(seed: String)| {
+                let account = Account::from_seed(&seed);
+                assert!(!account.private_key.is_zero());
+                assert!(!account.address().is_zero());
+
+                // verify signer can be recreated
+                let signer = account.signer();
+                assert_eq!(account.address(), signer.address());
+            })
+        }
+
+        #[test]
+        fn account_from_seed_bytes() {
+            proptest!(|(seed: Vec<u8>)| {
+                let account = Account::from_seed_bytes(&seed);
+                assert!(!account.private_key.is_zero());
+                assert!(!account.address().is_zero());
+
+                // verify signer can be recreated
+                let signer = account.signer();
+                assert_eq!(account.address(), signer.address());
+            })
+        }
+
+        #[test]
+        fn account_random() {
+            let account = Account::random();
+            assert!(!account.private_key.is_zero());
+            assert!(!account.address().is_zero());
+
+            // verify signer can be recreated
+            let signer = account.signer();
+            assert_eq!(account.address(), signer.address());
+        }
+    }
+
+    mod from_tag {
path: crates/motsu/src/context.rs
user: login: 0xNeshi
body: You're right, converted all proptests to regular ones.

I'd say `replace_with_tags` would need a unit test or two, but that applies to many functions within motsu. Nevertheless, I think this is outside the scope of this PR
created_at: 2025-03-13T11:55:35Z
author_association: COLLABORATOR
diff_hunk: @@ -961,29 +1028,136 @@ impl<ST: StorageType + VMRouter + 'static> Funding for Contract<ST> {
     }
 }
 
-/// Deterministically derive from a string representation.
+/// Allows creating entities deterministically from a string identifier.
+///
+/// This trait enables consistent generation of addresses and contracts across
+/// test runs using meaningful string identifiers (tags).
 pub trait FromTag {
-    /// Deterministically derive inner address of `Self` from a string `tag`.
+    /// Creates an instance deterministically derived from the provided tag.
+    ///
+    /// The same tag will always produce the same entity.
     fn from_tag(tag: &str) -> Self;
 }
 
-impl FromTag for Address {
+impl FromTag for Account {
+    /// Creates an account derived from the tag string.
+    ///
+    /// Also registers the tag in the test context for debugging purposes.
     fn from_tag(tag: &str) -> Self {
-        let hash = Keccak256::new().update(tag.as_bytes()).finalize();
-        let address = Address::from_slice(&hash[..20]);
-        VMContext::current().set_tag(address, tag.to_string());
-        address
+        let account = Account::from_seed(tag);
+        VMContext::current().set_tag(account.address(), tag.to_string());
+        account
     }
 }
 
-impl FromTag for Account {
+impl FromTag for Address {
+    /// Creates an Ethereum address derived from the tag string.
+    ///
+    /// Also registers the tag in the test context for debugging purposes.
     fn from_tag(tag: &str) -> Self {
-        Account::new_at(Address::from_tag(tag))
+        Account::from_tag(tag).address()
     }
 }
 
 impl<ST: StorageType + VMRouter + 'static> FromTag for Contract<ST> {
+    /// Creates a contract at an address derived from the tag string.
+    ///
+    /// This allows deploying contracts to deterministic addresses for testing.
+    /// Also registers the tag in the test context for debugging purposes.
     fn from_tag(tag: &str) -> Self {
         Contract::new_at(Address::from_tag(tag))
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use alloy_primitives::private::proptest::proptest;
+    use stylus_sdk::prelude::*;
+
+    use super::{Account, Address, Contract, FromTag};
+    use crate::context::VMContext;
+
+    mod account {
+        use super::*;
+
+        #[test]
+        fn account_from_seed() {
+            proptest!(|(seed: String)| {
+                let account = Account::from_seed(&seed);
+                assert!(!account.private_key.is_zero());
+                assert!(!account.address().is_zero());
+
+                // verify signer can be recreated
+                let signer = account.signer();
+                assert_eq!(account.address(), signer.address());
+            })
+        }
+
+        #[test]
+        fn account_from_seed_bytes() {
+            proptest!(|(seed: Vec<u8>)| {
+                let account = Account::from_seed_bytes(&seed);
+                assert!(!account.private_key.is_zero());
+                assert!(!account.address().is_zero());
+
+                // verify signer can be recreated
+                let signer = account.signer();
+                assert_eq!(account.address(), signer.address());
+            })
+        }
+
+        #[test]
+        fn account_random() {
+            let account = Account::random();
+            assert!(!account.private_key.is_zero());
+            assert!(!account.address().is_zero());
+
+            // verify signer can be recreated
+            let signer = account.signer();
+            assert_eq!(account.address(), signer.address());
+        }
+    }
+
+    mod from_tag {
+        use super::*;
+
+        #[test]
+        fn account() {
+            proptest!(|(tag: String)| {
+                let account = Account::from_tag(&tag);
+                assert!(!account.private_key.is_zero());
+                assert!(!account.address().is_zero());
+
+                assert_eq!(Some(tag), VMContext::current().get_tag(account.address()));
+            })
+        }
+
+        #[test]
+        fn address() {
+            proptest!(|(tag: String)| {
+                let address = Address::from_tag(&tag);
+                assert!(!address.is_zero());
+
+                assert_eq!(Some(tag), VMContext::current().get_tag(address));
+            })
+        }
+
+        #[storage]
+        struct SomeContract;
+
+        #[public]
+        impl SomeContract {}
+
+        unsafe impl TopLevelStorage for SomeContract {}
+
+        #[test]
+        fn contract() {
+            proptest!(|(tag: String)| {
+                let contract = Contract::<SomeContract>::from_tag(&tag);
+                assert!(!contract.address().is_zero());
+
+                assert_eq!(Some(tag), VMContext::current().get_tag(contract.address()));
+            })
+        }
+    }
path: crates/motsu/src/context.rs
user: login: 0xNeshi
body: This is a similar discussion to the one we had about testing our keccak crypto utility, where it was a question of testing our wrapper around `tiny-keccak`. At the time I was defending having tests in our code due to almost non-existence of tests on `tiny-keccak`'s side, but later I've understood the point you were trying to make then.
Given that the returned signer instance is implemented in an external crate (`alloy`), we're relying on the external crate to verify its implementation's correctness, which given its popularity can be considered tested enough. It's then redundant to test this on our side.
created_at: 2025-03-13T12:02:55Z
author_association: COLLABORATOR
diff_hunk: @@ -903,23 +918,75 @@ impl From<Account> for Address {
 }
 
 impl Account {
-    /// Create a new account with the given `address`.
+    /// Creates an account with an address derived from the provided seed
+    /// string.
+    ///
+    /// The seed string is hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    ///
+    /// # Panics
+    ///
+    /// * If the signing key creation fails with the keccak hash of the seed
+    ///   (shouldn't happen).
     #[must_use]
-    pub const fn new_at(address: Address) -> Self {
-        Self { address }
+    pub fn from_seed(seed: &str) -> Self {
+        Self::from_seed_bytes(seed.as_bytes())
     }
 
-    /// Create a new account with random address.
+    /// Creates an account with an address derived from the provided seed bytes.
+    ///
+    /// The seed bytes are hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    ///
+    /// # Panics
+    ///
+    /// * If the signing key creation fails with the keccak hash of the seed
+    ///   (shouldn't happen).
+    #[must_use]
+    pub fn from_seed_bytes(seed_bytes: &[u8]) -> Self {
path: crates/motsu/src/context.rs
user: login: qalisander
body: Let's keep it then
created_at: 2025-03-13T12:07:51Z
author_association: MEMBER
diff_hunk: @@ -903,23 +918,75 @@ impl From<Account> for Address {
 }
 
 impl Account {
-    /// Create a new account with the given `address`.
+    /// Creates an account with an address derived from the provided seed
+    /// string.
+    ///
+    /// The seed string is hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    ///
+    /// # Panics
+    ///
+    /// * If the signing key creation fails with the keccak hash of the seed
+    ///   (shouldn't happen).
     #[must_use]
-    pub const fn new_at(address: Address) -> Self {
-        Self { address }
+    pub fn from_seed(seed: &str) -> Self {
+        Self::from_seed_bytes(seed.as_bytes())
     }
 
-    /// Create a new account with random address.
+    /// Creates an account with an address derived from the provided seed bytes.
+    ///
+    /// The seed bytes are hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    ///
+    /// # Panics
+    ///
+    /// * If the signing key creation fails with the keccak hash of the seed
+    ///   (shouldn't happen).
+    #[must_use]
+    pub fn from_seed_bytes(seed_bytes: &[u8]) -> Self {
path: crates/motsu/src/context.rs
user: login: 0xNeshi
body: Admittedly having `new_at` for `Account` was shortsighted :smile: 
created_at: 2025-03-13T12:13:21Z
author_association: COLLABORATOR
diff_hunk: @@ -903,23 +918,75 @@ impl From<Account> for Address {
 }
 
 impl Account {
-    /// Create a new account with the given `address`.
+    /// Creates an account with an address derived from the provided seed
+    /// string.
+    ///
+    /// The seed string is hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    ///
+    /// # Panics
+    ///
+    /// * If the signing key creation fails with the keccak hash of the seed
+    ///   (shouldn't happen).
     #[must_use]
-    pub const fn new_at(address: Address) -> Self {
-        Self { address }
+    pub fn from_seed(seed: &str) -> Self {
+        Self::from_seed_bytes(seed.as_bytes())
     }
 
-    /// Create a new account with random address.
+    /// Creates an account with an address derived from the provided seed bytes.
+    ///
+    /// The seed bytes are hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    ///
+    /// # Panics
+    ///
+    /// * If the signing key creation fails with the keccak hash of the seed
+    ///   (shouldn't happen).
+    #[must_use]
+    pub fn from_seed_bytes(seed_bytes: &[u8]) -> Self {
path: crates/motsu/src/context.rs
user: login: 0xNeshi
body: I think creating an account from some seed (of any native type) should be maintainable, simply for the fact that `alloy` exposes similar functions for creating `SigningKey`
created_at: 2025-03-13T12:17:04Z
author_association: COLLABORATOR
diff_hunk: @@ -936,71 +957,150 @@ impl From<Account> for Address {
 }
 
 impl Account {
-    /// Create a new account with the given `address`.
+    /// Creates an account with an address derived from the provided seed
+    /// string.
+    ///
+    /// The seed string is hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    #[must_use]
+    pub fn from_seed(seed: &str) -> Self {
+        Self::from_seed_slice(seed.as_bytes())
+    }
+
+    /// Creates an account with an address derived from the provided seed bytes.
+    ///
+    /// The seed bytes are hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    #[allow(clippy::missing_panics_doc)]
     #[must_use]
-    pub const fn new_at(address: Address) -> Self {
-        Self { address }
+    pub fn from_seed_slice(seed: &[u8]) -> Self {
+        let private_key_bytes = Keccak256::new().update(seed).finalize();
+        let address = create_signer(&private_key_bytes).address();
+        let private_key = private_key_bytes.into();
+        Self { address, private_key }
     }
 
-    /// Create a new account with random address.
+    /// Creates a new account with a randomly generated private key and address.
     #[must_use]
     pub fn random() -> Self {
-        Self::new_at(Address::random())
+        Self::from_seed_slice(B256::random().as_slice())
     }
 
     /// Get account's address.
     #[must_use]
     pub fn address(&self) -> Address {
         self.address
     }
+
+    /// Returns a signer that can be used to sign messages and transactions.
+    #[must_use]
+    pub fn signer(&self) -> PrivateKeySigner {
+        create_signer(self.private_key.as_slice())
+    }
 }
 
-/// Fund the account.
+#[inline]
path: crates/motsu/src/context.rs
user: login: qalisander
body: nit: I'm not sure we need this. Only make sense when we're working on low level. I bet `PrivateKeySigner::from_signing_key` will consume much more than any potential benefit of `#[inline]`
created_at: 2025-03-14T14:50:29Z
author_association: MEMBER
diff_hunk: @@ -936,71 +957,150 @@ impl From<Account> for Address {
 }
 
 impl Account {
-    /// Create a new account with the given `address`.
+    /// Creates an account with an address derived from the provided seed
+    /// string.
+    ///
+    /// The seed string is hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    #[must_use]
+    pub fn from_seed(seed: &str) -> Self {
+        Self::from_seed_slice(seed.as_bytes())
+    }
+
+    /// Creates an account with an address derived from the provided seed bytes.
+    ///
+    /// The seed bytes are hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    #[allow(clippy::missing_panics_doc)]
     #[must_use]
-    pub const fn new_at(address: Address) -> Self {
-        Self { address }
+    pub fn from_seed_slice(seed: &[u8]) -> Self {
+        let private_key_bytes = Keccak256::new().update(seed).finalize();
+        let address = create_signer(&private_key_bytes).address();
+        let private_key = private_key_bytes.into();
+        Self { address, private_key }
     }
 
-    /// Create a new account with random address.
+    /// Creates a new account with a randomly generated private key and address.
     #[must_use]
     pub fn random() -> Self {
-        Self::new_at(Address::random())
+        Self::from_seed_slice(B256::random().as_slice())
     }
 
     /// Get account's address.
     #[must_use]
     pub fn address(&self) -> Address {
         self.address
     }
+
+    /// Returns a signer that can be used to sign messages and transactions.
+    #[must_use]
+    pub fn signer(&self) -> PrivateKeySigner {
+        create_signer(self.private_key.as_slice())
+    }
 }
 
-/// Fund the account.
+#[inline]
+fn create_signer(private_key: &[u8]) -> PrivateKeySigner {
+    PrivateKeySigner::from_signing_key(
+        SigningKey::from_slice(private_key)
+            .expect("failed to create signing key"),
+    )
+}
+
+/// Allows funding account with the chain's native token.
 pub trait Funding {
-    /// Fund the account with the given `value`.
+    /// Fund the account with the specified amount of the chain's native token.
     fn fund(&self, value: U256);
-
-    /// Get the balance of the account.
-    fn balance(&self) -> U256;
 }
 
 impl Funding for Address {
     fn fund(&self, value: U256) {
         VMContext::current().add_assign_balance(*self, value);
     }
+}
 
-    fn balance(&self) -> U256 {
-        VMContext::current().balance(*self)
+impl Funding for Account {
+    fn fund(&self, value: U256) {
+        self.address().fund(value);
     }
 }
 
-impl Funding for Account {
+impl<ST: StorageType + VMRouter + 'static> Funding for Contract<ST> {
     fn fund(&self, value: U256) {
         self.address().fund(value);
     }
+}
 
+impl AddressVM for Account {
     fn balance(&self) -> U256 {
         self.address().balance()
     }
-}
 
-impl<ST: StorageType + VMRouter + 'static> Funding for Contract<ST> {
-    fn fund(&self, value: U256) {
-        self.address().fund(value);
+    fn code(&self) -> Vec<u8> {
+        self.address().code()
+    }
+
+    fn code_size(&self) -> usize {
+        self.address().code_size()
     }
 
+    fn code_hash(&self) -> B256 {
+        self.address().code_hash()
+    }
+
+    fn has_code(&self) -> bool {
+        self.address().has_code()
+    }
+}
+
+impl<ST: StorageType + VMRouter + 'static> AddressVM for Contract<ST> {
     fn balance(&self) -> U256 {
path: crates/motsu/src/context.rs
user: login: qalisander
body: Only `balance` and `has_code` here will return an actual valid result. We cannot make any assumptions around wasm since we even haven't compiled it.
I would just have a `Balance` trait with `balance()` function, implemented for `Account` and `Contract`.
For `Address` we can use already implemented `AddressVM`. And since it will correspond to user account address in our tests, `code_size` and `code_hash` will be zero anyways or predetermined
created_at: 2025-03-14T14:56:18Z
author_association: MEMBER
diff_hunk: @@ -936,71 +957,150 @@ impl From<Account> for Address {
 }
 
 impl Account {
-    /// Create a new account with the given `address`.
+    /// Creates an account with an address derived from the provided seed
+    /// string.
+    ///
+    /// The seed string is hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    #[must_use]
+    pub fn from_seed(seed: &str) -> Self {
+        Self::from_seed_slice(seed.as_bytes())
+    }
+
+    /// Creates an account with an address derived from the provided seed bytes.
+    ///
+    /// The seed bytes are hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    #[allow(clippy::missing_panics_doc)]
     #[must_use]
-    pub const fn new_at(address: Address) -> Self {
-        Self { address }
+    pub fn from_seed_slice(seed: &[u8]) -> Self {
+        let private_key_bytes = Keccak256::new().update(seed).finalize();
+        let address = create_signer(&private_key_bytes).address();
+        let private_key = private_key_bytes.into();
+        Self { address, private_key }
     }
 
-    /// Create a new account with random address.
+    /// Creates a new account with a randomly generated private key and address.
     #[must_use]
     pub fn random() -> Self {
-        Self::new_at(Address::random())
+        Self::from_seed_slice(B256::random().as_slice())
     }
 
     /// Get account's address.
     #[must_use]
     pub fn address(&self) -> Address {
         self.address
     }
+
+    /// Returns a signer that can be used to sign messages and transactions.
+    #[must_use]
+    pub fn signer(&self) -> PrivateKeySigner {
+        create_signer(self.private_key.as_slice())
+    }
 }
 
-/// Fund the account.
+#[inline]
path: crates/motsu/src/context.rs
user: login: qalisander
body: And rust is able to do inline by itself
created_at: 2025-03-14T15:03:00Z
author_association: MEMBER
diff_hunk: @@ -39,10 +42,15 @@ use crate::{
 static MOTSU_VM: Lazy<DashMap<VMContext, VMContextStorage>> =
     Lazy::new(DashMap::new);
 
-/// Same value as [`stylus_sdk::testing::constants::DEFAULT_CHAIN_ID`].
+/// Same value as
+/// [`stylus_sdk::testing::constants::DEFAULT_CHAIN_ID`][chain_id].
+///
 /// We'll be able to remove this after we can enable the `stylus-test` feature,
-/// which should happen after we implement the [`stylus_sdk::testing::Host`]
-/// trait.
+/// which should happen after we refactor `motsu` to implement a mock
path: crates/motsu/src/context.rs
user: login: qalisander
body: I don't get why we need to remove `DEFAULT_CHAIN_ID` later. Can't we keep the description more minimalistic?
created_at: 2025-03-14T15:13:04Z
author_association: MEMBER
diff_hunk: @@ -936,71 +957,150 @@ impl From<Account> for Address {
 }
 
 impl Account {
-    /// Create a new account with the given `address`.
+    /// Creates an account with an address derived from the provided seed
+    /// string.
+    ///
+    /// The seed string is hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    #[must_use]
+    pub fn from_seed(seed: &str) -> Self {
+        Self::from_seed_slice(seed.as_bytes())
+    }
+
+    /// Creates an account with an address derived from the provided seed bytes.
+    ///
+    /// The seed bytes are hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    #[allow(clippy::missing_panics_doc)]
     #[must_use]
-    pub const fn new_at(address: Address) -> Self {
-        Self { address }
+    pub fn from_seed_slice(seed: &[u8]) -> Self {
+        let private_key_bytes = Keccak256::new().update(seed).finalize();
+        let address = create_signer(&private_key_bytes).address();
+        let private_key = private_key_bytes.into();
+        Self { address, private_key }
     }
 
-    /// Create a new account with random address.
+    /// Creates a new account with a randomly generated private key and address.
     #[must_use]
     pub fn random() -> Self {
-        Self::new_at(Address::random())
+        Self::from_seed_slice(B256::random().as_slice())
     }
 
     /// Get account's address.
     #[must_use]
     pub fn address(&self) -> Address {
         self.address
     }
+
+    /// Returns a signer that can be used to sign messages and transactions.
+    #[must_use]
+    pub fn signer(&self) -> PrivateKeySigner {
+        create_signer(self.private_key.as_slice())
+    }
 }
 
-/// Fund the account.
+#[inline]
+fn create_signer(private_key: &[u8]) -> PrivateKeySigner {
+    PrivateKeySigner::from_signing_key(
+        SigningKey::from_slice(private_key)
+            .expect("failed to create signing key"),
+    )
+}
+
+/// Allows funding account with the chain's native token.
 pub trait Funding {
-    /// Fund the account with the given `value`.
+    /// Fund the account with the specified amount of the chain's native token.
     fn fund(&self, value: U256);
-
-    /// Get the balance of the account.
-    fn balance(&self) -> U256;
 }
 
 impl Funding for Address {
     fn fund(&self, value: U256) {
         VMContext::current().add_assign_balance(*self, value);
     }
+}
 
-    fn balance(&self) -> U256 {
-        VMContext::current().balance(*self)
+impl Funding for Account {
+    fn fund(&self, value: U256) {
+        self.address().fund(value);
     }
 }
 
-impl Funding for Account {
+impl<ST: StorageType + VMRouter + 'static> Funding for Contract<ST> {
     fn fund(&self, value: U256) {
         self.address().fund(value);
     }
+}
 
+impl AddressVM for Account {
     fn balance(&self) -> U256 {
         self.address().balance()
     }
-}
 
-impl<ST: StorageType + VMRouter + 'static> Funding for Contract<ST> {
-    fn fund(&self, value: U256) {
-        self.address().fund(value);
+    fn code(&self) -> Vec<u8> {
+        self.address().code()
+    }
+
+    fn code_size(&self) -> usize {
+        self.address().code_size()
     }
 
+    fn code_hash(&self) -> B256 {
+        self.address().code_hash()
+    }
+
+    fn has_code(&self) -> bool {
+        self.address().has_code()
+    }
+}
+
+impl<ST: StorageType + VMRouter + 'static> AddressVM for Contract<ST> {
     fn balance(&self) -> U256 {
path: crates/motsu/src/context.rs
user: login: 0xNeshi
body: > We cannot make any assumptions around wasm since we even haven't compiled it.

This is the main problem :point_up: Makes sense.

 >  I would just have a Balance trait with balance() function, implemented for Account and Contract

I actually [had this](https://github.com/OpenZeppelin/stylus-test-helpers/pull/72/commits/07182f4df2ef013e8e4e5c5ae7e6e067b7058df5) implemented previously, will revert to it :smile: 
created_at: 2025-03-14T15:54:16Z
author_association: COLLABORATOR
diff_hunk: @@ -936,71 +957,150 @@ impl From<Account> for Address {
 }
 
 impl Account {
-    /// Create a new account with the given `address`.
+    /// Creates an account with an address derived from the provided seed
+    /// string.
+    ///
+    /// The seed string is hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    #[must_use]
+    pub fn from_seed(seed: &str) -> Self {
+        Self::from_seed_slice(seed.as_bytes())
+    }
+
+    /// Creates an account with an address derived from the provided seed bytes.
+    ///
+    /// The seed bytes are hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    #[allow(clippy::missing_panics_doc)]
     #[must_use]
-    pub const fn new_at(address: Address) -> Self {
-        Self { address }
+    pub fn from_seed_slice(seed: &[u8]) -> Self {
+        let private_key_bytes = Keccak256::new().update(seed).finalize();
+        let address = create_signer(&private_key_bytes).address();
+        let private_key = private_key_bytes.into();
+        Self { address, private_key }
     }
 
-    /// Create a new account with random address.
+    /// Creates a new account with a randomly generated private key and address.
     #[must_use]
     pub fn random() -> Self {
-        Self::new_at(Address::random())
+        Self::from_seed_slice(B256::random().as_slice())
     }
 
     /// Get account's address.
     #[must_use]
     pub fn address(&self) -> Address {
         self.address
     }
+
+    /// Returns a signer that can be used to sign messages and transactions.
+    #[must_use]
+    pub fn signer(&self) -> PrivateKeySigner {
+        create_signer(self.private_key.as_slice())
+    }
 }
 
-/// Fund the account.
+#[inline]
+fn create_signer(private_key: &[u8]) -> PrivateKeySigner {
+    PrivateKeySigner::from_signing_key(
+        SigningKey::from_slice(private_key)
+            .expect("failed to create signing key"),
+    )
+}
+
+/// Allows funding account with the chain's native token.
 pub trait Funding {
-    /// Fund the account with the given `value`.
+    /// Fund the account with the specified amount of the chain's native token.
     fn fund(&self, value: U256);
-
-    /// Get the balance of the account.
-    fn balance(&self) -> U256;
 }
 
 impl Funding for Address {
     fn fund(&self, value: U256) {
         VMContext::current().add_assign_balance(*self, value);
     }
+}
 
-    fn balance(&self) -> U256 {
-        VMContext::current().balance(*self)
+impl Funding for Account {
+    fn fund(&self, value: U256) {
+        self.address().fund(value);
     }
 }
 
-impl Funding for Account {
+impl<ST: StorageType + VMRouter + 'static> Funding for Contract<ST> {
     fn fund(&self, value: U256) {
         self.address().fund(value);
     }
+}
 
+impl AddressVM for Account {
     fn balance(&self) -> U256 {
         self.address().balance()
     }
-}
 
-impl<ST: StorageType + VMRouter + 'static> Funding for Contract<ST> {
-    fn fund(&self, value: U256) {
-        self.address().fund(value);
+    fn code(&self) -> Vec<u8> {
+        self.address().code()
+    }
+
+    fn code_size(&self) -> usize {
+        self.address().code_size()
     }
 
+    fn code_hash(&self) -> B256 {
+        self.address().code_hash()
+    }
+
+    fn has_code(&self) -> bool {
+        self.address().has_code()
+    }
+}
+
+impl<ST: StorageType + VMRouter + 'static> AddressVM for Contract<ST> {
     fn balance(&self) -> U256 {
path: crates/motsu/src/context.rs
user: login: 0xNeshi
body: There will still be a problem - devs can still do:
```rust
    #[motsu::test]
    fn external_call_panic(
        ping: Contract<PingContract>,
        alice: Address,
    ) {
        let code_size = ping.address().code_size();
        let code_hash = ping.address().code_hash();
    }
```
which will both return invalid values. I hope this is solved in the new stylus sdk version with `Host`.
created_at: 2025-03-14T16:00:10Z
author_association: COLLABORATOR
diff_hunk: @@ -39,6 +42,9 @@ use crate::{
 static MOTSU_VM: Lazy<DashMap<VMContext, VMContextStorage>> =
     Lazy::new(DashMap::new);
 
+// TODO: remove this after we can enable the `stylus-test` feature, which should
+// happen after we refactor `motsu` to implement a mock
+// `stylus_core::host::Host` trait.
 /// Arbitrum one chain id from [chain info].
 ///
 /// [chain info]: https://docs.arbitrum.io/for-devs/dev-tools-and-resources/chain-info
path: crates/motsu/src/context.rs
user: login: 0xNeshi
body: Is this TODO ok with you @qalisander ? I think after we switch to `Host` we can just use their `stylus_sdk::testing::constants::DEFAULT_CHAIN_ID` for compatibility and easier maintenance
created_at: 2025-03-14T16:04:26Z
author_association: COLLABORATOR
diff_hunk: @@ -936,71 +957,150 @@ impl From<Account> for Address {
 }
 
 impl Account {
-    /// Create a new account with the given `address`.
+    /// Creates an account with an address derived from the provided seed
+    /// string.
+    ///
+    /// The seed string is hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    #[must_use]
+    pub fn from_seed(seed: &str) -> Self {
+        Self::from_seed_slice(seed.as_bytes())
+    }
+
+    /// Creates an account with an address derived from the provided seed bytes.
+    ///
+    /// The seed bytes are hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    #[allow(clippy::missing_panics_doc)]
     #[must_use]
-    pub const fn new_at(address: Address) -> Self {
-        Self { address }
+    pub fn from_seed_slice(seed: &[u8]) -> Self {
+        let private_key_bytes = Keccak256::new().update(seed).finalize();
+        let address = create_signer(&private_key_bytes).address();
+        let private_key = private_key_bytes.into();
+        Self { address, private_key }
     }
 
-    /// Create a new account with random address.
+    /// Creates a new account with a randomly generated private key and address.
     #[must_use]
     pub fn random() -> Self {
-        Self::new_at(Address::random())
+        Self::from_seed_slice(B256::random().as_slice())
     }
 
     /// Get account's address.
     #[must_use]
     pub fn address(&self) -> Address {
         self.address
     }
+
+    /// Returns a signer that can be used to sign messages and transactions.
+    #[must_use]
+    pub fn signer(&self) -> PrivateKeySigner {
+        create_signer(self.private_key.as_slice())
+    }
 }
 
-/// Fund the account.
+#[inline]
+fn create_signer(private_key: &[u8]) -> PrivateKeySigner {
+    PrivateKeySigner::from_signing_key(
+        SigningKey::from_slice(private_key)
+            .expect("failed to create signing key"),
+    )
+}
+
+/// Allows funding account with the chain's native token.
 pub trait Funding {
-    /// Fund the account with the given `value`.
+    /// Fund the account with the specified amount of the chain's native token.
     fn fund(&self, value: U256);
-
-    /// Get the balance of the account.
-    fn balance(&self) -> U256;
 }
 
 impl Funding for Address {
     fn fund(&self, value: U256) {
         VMContext::current().add_assign_balance(*self, value);
     }
+}
 
-    fn balance(&self) -> U256 {
-        VMContext::current().balance(*self)
+impl Funding for Account {
+    fn fund(&self, value: U256) {
+        self.address().fund(value);
     }
 }
 
-impl Funding for Account {
+impl<ST: StorageType + VMRouter + 'static> Funding for Contract<ST> {
     fn fund(&self, value: U256) {
         self.address().fund(value);
     }
+}
 
+impl AddressVM for Account {
     fn balance(&self) -> U256 {
         self.address().balance()
     }
-}
 
-impl<ST: StorageType + VMRouter + 'static> Funding for Contract<ST> {
-    fn fund(&self, value: U256) {
-        self.address().fund(value);
+    fn code(&self) -> Vec<u8> {
+        self.address().code()
+    }
+
+    fn code_size(&self) -> usize {
+        self.address().code_size()
     }
 
+    fn code_hash(&self) -> B256 {
+        self.address().code_hash()
+    }
+
+    fn has_code(&self) -> bool {
+        self.address().has_code()
+    }
+}
+
+impl<ST: StorageType + VMRouter + 'static> AddressVM for Contract<ST> {
     fn balance(&self) -> U256 {
path: crates/motsu/src/context.rs
user: login: qalisander
body: Stylus sdk `Host` just lets us remove shims and nothing more. We are not compiling wasm contract in tests to calculate binary's hash or size. And it will be very hard to do it in unit tests.

We can solve this problem with an additional mocking api like `ping.set_code_size()` or `pint.set_code_hash()`. But let's better leave it for later.

created_at: 2025-03-14T16:12:10Z
author_association: MEMBER
diff_hunk: @@ -936,71 +957,150 @@ impl From<Account> for Address {
 }
 
 impl Account {
-    /// Create a new account with the given `address`.
+    /// Creates an account with an address derived from the provided seed
+    /// string.
+    ///
+    /// The seed string is hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    #[must_use]
+    pub fn from_seed(seed: &str) -> Self {
+        Self::from_seed_slice(seed.as_bytes())
+    }
+
+    /// Creates an account with an address derived from the provided seed bytes.
+    ///
+    /// The seed bytes are hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    #[allow(clippy::missing_panics_doc)]
     #[must_use]
-    pub const fn new_at(address: Address) -> Self {
-        Self { address }
+    pub fn from_seed_slice(seed: &[u8]) -> Self {
+        let private_key_bytes = Keccak256::new().update(seed).finalize();
+        let address = create_signer(&private_key_bytes).address();
+        let private_key = private_key_bytes.into();
+        Self { address, private_key }
     }
 
-    /// Create a new account with random address.
+    /// Creates a new account with a randomly generated private key and address.
     #[must_use]
     pub fn random() -> Self {
-        Self::new_at(Address::random())
+        Self::from_seed_slice(B256::random().as_slice())
     }
 
     /// Get account's address.
     #[must_use]
     pub fn address(&self) -> Address {
         self.address
     }
+
+    /// Returns a signer that can be used to sign messages and transactions.
+    #[must_use]
+    pub fn signer(&self) -> PrivateKeySigner {
+        create_signer(self.private_key.as_slice())
+    }
 }
 
-/// Fund the account.
+#[inline]
+fn create_signer(private_key: &[u8]) -> PrivateKeySigner {
+    PrivateKeySigner::from_signing_key(
+        SigningKey::from_slice(private_key)
+            .expect("failed to create signing key"),
+    )
+}
+
+/// Allows funding account with the chain's native token.
 pub trait Funding {
-    /// Fund the account with the given `value`.
+    /// Fund the account with the specified amount of the chain's native token.
     fn fund(&self, value: U256);
-
-    /// Get the balance of the account.
-    fn balance(&self) -> U256;
 }
 
 impl Funding for Address {
     fn fund(&self, value: U256) {
         VMContext::current().add_assign_balance(*self, value);
     }
+}
 
-    fn balance(&self) -> U256 {
-        VMContext::current().balance(*self)
+impl Funding for Account {
+    fn fund(&self, value: U256) {
+        self.address().fund(value);
     }
 }
 
-impl Funding for Account {
+impl<ST: StorageType + VMRouter + 'static> Funding for Contract<ST> {
     fn fund(&self, value: U256) {
         self.address().fund(value);
     }
+}
 
+impl AddressVM for Account {
     fn balance(&self) -> U256 {
         self.address().balance()
     }
-}
 
-impl<ST: StorageType + VMRouter + 'static> Funding for Contract<ST> {
-    fn fund(&self, value: U256) {
-        self.address().fund(value);
+    fn code(&self) -> Vec<u8> {
+        self.address().code()
+    }
+
+    fn code_size(&self) -> usize {
+        self.address().code_size()
     }
 
+    fn code_hash(&self) -> B256 {
+        self.address().code_hash()
+    }
+
+    fn has_code(&self) -> bool {
+        self.address().has_code()
+    }
+}
+
+impl<ST: StorageType + VMRouter + 'static> AddressVM for Contract<ST> {
     fn balance(&self) -> U256 {
path: crates/motsu/src/context.rs
user: login: qalisander
body: Since `code_hash()` for contracts is out of use at the moment, let's not enforce it for `Contract<..>`:D
created_at: 2025-03-14T16:13:23Z
author_association: MEMBER
diff_hunk: @@ -39,6 +42,9 @@ use crate::{
 static MOTSU_VM: Lazy<DashMap<VMContext, VMContextStorage>> =
     Lazy::new(DashMap::new);
 
+// TODO: remove this after we can enable the `stylus-test` feature, which should
+// happen after we refactor `motsu` to implement a mock
+// `stylus_core::host::Host` trait.
 /// Arbitrum one chain id from [chain info].
 ///
 /// [chain info]: https://docs.arbitrum.io/for-devs/dev-tools-and-resources/chain-info
path: crates/motsu/src/context.rs
user: login: qalisander
body: But we will still keep this constant for mocking purposes. We should have smth for `chain_id` by default when we implement `Host` trait.
created_at: 2025-03-14T16:15:45Z
author_association: MEMBER
diff_hunk: @@ -1,7 +1,7 @@
 //! Common imports for `motsu` tests.
 pub use crate::{
     context::{
-        Account, Contract, ContractCall, FromTag, Funding, VMContext,
+        Account, Balance, Contract, ContractCall, FromTag, Funding, VMContext,
         DEFAULT_CHAIN_ID,
path: crates/motsu/src/prelude.rs
user: login: qalisander
body: nit: If we gonna export `DEFAULT_CHAIN_ID` for consumers, we can consider to have a more specific name like `ARBITRUM_ONE_CHAIN_ID`. I think we're missing public `VMContext::curent().chain_id()`
created_at: 2025-03-14T16:22:36Z
author_association: MEMBER
diff_hunk: @@ -935,71 +954,123 @@ impl From<Account> for Address {
 }
 
 impl Account {
-    /// Create a new account with the given `address`.
+    /// Creates an account with an address derived from the provided seed
+    /// string.
+    ///
+    /// The seed string is hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
     #[must_use]
-    pub const fn new_at(address: Address) -> Self {
-        Self { address }
+    pub fn from_seed(seed: &str) -> Self {
+        Self::from_seed_slice(seed.as_bytes())
     }
 
-    /// Create a new account with random address.
+    /// Creates an account with an address derived from the provided seed bytes.
+    ///
+    /// The seed bytes are hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    #[allow(clippy::missing_panics_doc)]
+    #[must_use]
+    pub fn from_seed_slice(seed: &[u8]) -> Self {
+        let private_key_bytes = Keccak256::new().update(seed).finalize();
+        let address = create_signer(&private_key_bytes).address();
+        let private_key = private_key_bytes.into();
+        Self { address, private_key }
+    }
+
+    /// Creates a new account with a randomly generated private key and address.
     #[must_use]
     pub fn random() -> Self {
-        Self::new_at(Address::random())
+        Self::from_seed_slice(B256::random().as_slice())
     }
 
     /// Get account's address.
     #[must_use]
     pub fn address(&self) -> Address {
         self.address
     }
+
+    /// Returns a signer that can be used to sign messages and transactions.
+    #[must_use]
+    pub fn signer(&self) -> PrivateKeySigner {
+        create_signer(self.private_key.as_slice())
+    }
+}
+
+fn create_signer(private_key: &[u8]) -> PrivateKeySigner {
path: crates/motsu/src/context.rs
user: login: qalisander
body: nit: If it would be my pr, I guess you would ask me to add docs. But since it is a private function I don't care much:D
created_at: 2025-03-14T16:24:23Z
author_association: MEMBER
diff_hunk: @@ -1,7 +1,7 @@
 //! Common imports for `motsu` tests.
 pub use crate::{
     context::{
-        Account, Contract, ContractCall, FromTag, Funding, VMContext,
+        Account, Balance, Contract, ContractCall, FromTag, Funding, VMContext,
         DEFAULT_CHAIN_ID,
path: crates/motsu/src/prelude.rs
user: login: qalisander
body: Actually that is why I like to have everything private: can care less about breaking changes :D
created_at: 2025-03-14T16:25:44Z
author_association: MEMBER
diff_hunk: @@ -33,22 +30,66 @@ mod tests {
 }
 ```
 
+If you need to instantiate an accound that contains a signer and a private key,
+you can use `Account` instead of `Address`:
+
+```rust
+#[cfg(test)]
+mod tests {
+    use motsu::prelude::*;
+    use alloy_signer::SignerSync;
+
+    #[motsu::test]
+    fn signs_message(alice: Account) {
+        let msg = "message".as_bytes();
+        let signer = alice.signer();
+        assert!(signer.sign_message_sync(msg).is_ok());
+    }
+}
+```
+
+### Global Variables
+
+Motsu allows you to manipulate certain global variables that affect the
+execution environment:
+
+#### Chain ID
+
+You can get and set the Chain ID in tests using the `VMContext` API:
path: crates/motsu/README.md
user: login: qalisander
body: Se yeah. At the moment we can not access `VMContext::current().chain_id()` publicly.
created_at: 2025-03-14T16:33:14Z
author_association: MEMBER
diff_hunk: @@ -39,6 +42,9 @@ use crate::{
 static MOTSU_VM: Lazy<DashMap<VMContext, VMContextStorage>> =
     Lazy::new(DashMap::new);
 
+// TODO: remove this after we can enable the `stylus-test` feature, which should
+// happen after we refactor `motsu` to implement a mock
+// `stylus_core::host::Host` trait.
 /// Arbitrum one chain id from [chain info].
 ///
 /// [chain info]: https://docs.arbitrum.io/for-devs/dev-tools-and-resources/chain-info
path: crates/motsu/src/context.rs
user: login: qalisander
body: Also if you think we gonna remove it, there is no need to make it public then [here](https://github.com/OpenZeppelin/stylus-test-helpers/pull/72#discussion_r1995872823)
created_at: 2025-03-14T16:43:09Z
author_association: MEMBER
diff_hunk: @@ -39,6 +42,9 @@ use crate::{
 static MOTSU_VM: Lazy<DashMap<VMContext, VMContextStorage>> =
     Lazy::new(DashMap::new);
 
+// TODO: remove this after we can enable the `stylus-test` feature, which should
+// happen after we refactor `motsu` to implement a mock
+// `stylus_core::host::Host` trait.
 /// Arbitrum one chain id from [chain info].
 ///
 /// [chain info]: https://docs.arbitrum.io/for-devs/dev-tools-and-resources/chain-info
path: crates/motsu/src/context.rs
user: login: 0xNeshi
body: Will make it private as it's temporary, devs can use `stylus_sdk::testing::constants::DEFAULT_CHAIN_ID` if they want the actual constant
created_at: 2025-03-17T07:34:53Z
author_association: COLLABORATOR
diff_hunk: @@ -935,71 +954,123 @@ impl From<Account> for Address {
 }
 
 impl Account {
-    /// Create a new account with the given `address`.
+    /// Creates an account with an address derived from the provided seed
+    /// string.
+    ///
+    /// The seed string is hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
     #[must_use]
-    pub const fn new_at(address: Address) -> Self {
-        Self { address }
+    pub fn from_seed(seed: &str) -> Self {
+        Self::from_seed_slice(seed.as_bytes())
     }
 
-    /// Create a new account with random address.
+    /// Creates an account with an address derived from the provided seed bytes.
+    ///
+    /// The seed bytes are hashed with Keccak256 to generate the private key.
+    /// The same seed will always produce the same account address.
+    #[allow(clippy::missing_panics_doc)]
+    #[must_use]
+    pub fn from_seed_slice(seed: &[u8]) -> Self {
+        let private_key_bytes = Keccak256::new().update(seed).finalize();
+        let address = create_signer(&private_key_bytes).address();
+        let private_key = private_key_bytes.into();
+        Self { address, private_key }
+    }
+
+    /// Creates a new account with a randomly generated private key and address.
     #[must_use]
     pub fn random() -> Self {
-        Self::new_at(Address::random())
+        Self::from_seed_slice(B256::random().as_slice())
     }
 
     /// Get account's address.
     #[must_use]
     pub fn address(&self) -> Address {
         self.address
     }
+
+    /// Returns a signer that can be used to sign messages and transactions.
+    #[must_use]
+    pub fn signer(&self) -> PrivateKeySigner {
+        create_signer(self.private_key.as_slice())
+    }
+}
+
+fn create_signer(private_key: &[u8]) -> PrivateKeySigner {
path: crates/motsu/src/context.rs
user: login: 0xNeshi
body: I'd ask for comments if it weren't immediately apparent what the code did :D 
I think the function name (and its code) are clear enough, but will add a comment for convenience :+1: 
created_at: 2025-03-17T07:39:05Z
author_association: COLLABORATOR
diff_hunk: @@ -33,22 +30,66 @@ mod tests {
 }
 ```
 
+If you need to instantiate an accound that contains a signer and a private key,
+you can use `Account` instead of `Address`:
+
+```rust
+#[cfg(test)]
+mod tests {
+    use motsu::prelude::*;
+    use alloy_signer::SignerSync;
+
+    #[motsu::test]
+    fn signs_message(alice: Account) {
+        let msg = "message".as_bytes();
+        let signer = alice.signer();
+        assert!(signer.sign_message_sync(msg).is_ok());
+    }
+}
+```
+
+### Global Variables
+
+Motsu allows you to manipulate certain global variables that affect the
+execution environment:
+
+#### Chain ID
+
+You can get and set the Chain ID in tests using the `VMContext` API:
path: crates/motsu/README.md
user: login: 0xNeshi
body: Removed mention of "get" (chain id). Makes little sense to make `VMContext::current().chain_id()` public as users will use `block::chainid()` for that.
created_at: 2025-03-17T07:42:25Z
author_association: COLLABORATOR

PR comments:
user: login: 0xNeshi
created_at: 2025-03-13T08:55:00Z
body: [`check-publish` job failure](https://github.com/OpenZeppelin/stylus-test-helpers/actions/runs/13811894096/job/38635184226?pr=72) will be addressed in https://github.com/OpenZeppelin/stylus-test-helpers/pull/73#pullrequestreview-2681030811
author_association: COLLABORATOR
user: login: qalisander
created_at: 2025-03-13T10:49:46Z
body: @0xNeshi This feature seems noticeably slowing down `proxies_tests`. You can check out against main branch. We should think when we gonna add proptesting support for motsu, this signer will be like a bottleneck
author_association: MEMBER
user: login: 0xNeshi
created_at: 2025-03-13T12:20:30Z
body: > @0xNeshi This feature seems noticeably slowing down `proxies_tests`. You can check out against main branch. We should think when we gonna add protesting support for motsu, this signer will be like a bottleneck

This is concerning. I just benchmarked locally and each test takes a couple of ms longer to execute, which accumulates so much that `no_locks_with_panics` takes around 10x times longer :scream: 
I can spend a bit more time thinking of an alternative design that doesn't require creating a `PrivateKeySigner` just to derive an account address.
author_association: COLLABORATOR
user: login: qalisander
created_at: 2025-03-13T12:37:43Z
body: Here is profiling benchmark:

<img width="1097" alt="image" src="https://github.com/user-attachments/assets/b131a75b-c580-45fb-86a4-85f5b6d0f595" />

Key pair generation consumes 82% time of the test. We don't care about benchmarks for tests at the moment, but there is always a limit;) Signing should be optional in this case

author_association: MEMBER
user: login: qalisander
created_at: 2025-03-13T12:51:17Z
body: The options we have:
- Create another type `SignerAccount` that will contain signing feature. Hopefully `.sender(..)` function has `AsRef<Address>` argument and it should work without modifications. And we will have no breaking changes with `new_at` in this case.
- Similar to previous, but add generic parameter with default setting to account like: `struct Account<T: AccountConfig = NoSigner>`
- Don't use `Account` but use `Address` for most of the tests. And `Account` for signatures. (we will probably have a problem with collision of `balance()` functions from `stylus-sdk` and `motsu` for `Address`)
author_association: MEMBER
user: login: qalisander
created_at: 2025-03-13T15:08:31Z
body: I think we have this results because we're not doing a lot in our test cases. Just simple operations. And generation of keypare consumes more just in comparison to logic in our simple test cases. We should check this branch with our library test cases first and then think
author_association: MEMBER
user: login: 0xNeshi
created_at: 2025-03-14T07:24:05Z
body: > The options we have:
> 
> * Create another type `SignerAccount` that will contain signing feature. Hopefully `.sender(..)` function has `AsRef<Address>` argument and it should work without modifications. And we will have no breaking changes with `new_at` in this case.
> 
> * Similar to previous, but add generic parameter with default setting to account like: `struct Account<T: AccountConfig = NoSigner>`
> 
> * Don't use `Account` but use `Address` for most of the tests. And `Account` for signatures. (we will probably have a problem with collision of `balance()` functions from `stylus-sdk` and `motsu` for `Address`)

I had the exact same options pop into my head yesterday, unbelievable :muscle: :smile: 

I think the last approach is the cleanest. Currently `Account` is no different than `Address`, so they're interchangeable. By including a signer in `Account`, we give its existence meaning.
The conflict you mentioned is referring to [AddressVM::balance](https://github.com/OffchainLabs/stylus-sdk-rs/blob/b1993362d214b27ceb67e0aa01ee2a6397f376b4/stylus-sdk/src/types.rs#L69-L74), right? ~~If so, this should be rare, as `Funding::balance` is imported in tests by default (with `motsu::prelude`), while `AddressVM` needs to be explicitly and consciously imported; if a dev does that, they're responsible for the (bad) result.~~ I forgot `AddressVM` is imported as part of `stylus_sdk::prelude`, so the conflict is always there. Will think about this.

EDIT: it seems we always had this problem with conflicting `balance` on `Address`.
author_association: COLLABORATOR
user: login: 0xNeshi
created_at: 2025-03-14T09:23:08Z
body: @qalisander check out the proposed approach to implement [3rd proposal](https://github.com/OpenZeppelin/stylus-test-helpers/pull/72#issuecomment-2721151904) while avoiding the method conflict.
In short:
- Removed `Funding::balance`
- Implemented `AddressVM` for `Account` and `Contract<T>`

Tests needed no updates except for turning all `Account`s into `Address`es.

About the test performance, I updated our tests to use `alice: Address` and the exec. speed is now the same as before. I also updated the docs to recommend using `Address` while mentioning when to use `Account`.

Wdyt?
author_association: COLLABORATOR
user: login: qalisander
created_at: 2025-03-14T11:55:53Z
body: Yeah. Good job! We will be using `stylus-sdk`'s `balance` instead. I've managed to check the previous version in our library. And it seems test pass time just slightly increased (when you've been generating `Address` from `Account` keypair). Should be fine even if we're using `Account` I guess
author_association: MEMBER