-
Notifications
You must be signed in to change notification settings - Fork 48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: convenience functions for Contract
and Account
#33
Conversation
Account::call
using call builderContract
and Account
More of a context question:
What would the method be for mainnet support? |
@ChaoticTempest shouldn't it be |
@TrevorJTClarke Equivalent in mainnet would be Creating a sub account should look like this: let subaccount = account.create_account("subaccount")
.with_initial_balance(parse_near!("1 N")); // optional: defaults to 0.1 Near. Enough to hold storage
.with_key(secret_key) // optional: if not specified, generate it
.transact()
.await?;
println!("account id: {:?}", account.id()); // should yield "dev-{NUM}"
println!("subaccount id: {:?}", subaccount.id()); // should yield "subaccount.dev-{NUM}"
You mean how we reference |
workspaces/src/network/account.rs
Outdated
parent_id: AccountId, | ||
new_account_id: String, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little confused on why we are forcing the account to be built as a subaccount. If this is specifically intended for that flow, perhaps we should rename to indicate this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll just rename it to be create_subaccount
then. This was initially to be consistent with workspaces-js/Account
, but better to be explicit here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is the fact that it's a subaccount obfuscated completely from the API? The only issue I see is just that it may be doing something differently than a developer may expect, which is a bit of a foot gun. Maybe we should consider renaming on js side for consistency as well if we want to make this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is the fact that it's a subaccount obfuscated completely from the API?
Yup, it's pretty much that. When we go to call into account creation, there's no flag to say, "hey let's go make a subaccount". Instead, it's just based on account id we give it such as "A" or "B.A"
Maybe we should consider renaming on js side for consistency as well if we want to make this change?
The js side is pretty flexible (maybe too flexible). The create_account
on that side allows you to create both top level and subaccounts regardless of what account
context is present. Changing it on the js side would probably require a good amount of rework. On rust side, I've just had it be explicit in the functionality. If you want to create a TLA account you call worker.create_tla
, and subaccounts with account.create_subaccount
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
okay, I think I might have to look through code when I get a chance to understand this difference, thanks for giving context!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM overall!
There's some awkwardness in API here and there, which is totally fine for the current state of iteration!
workspaces/src/network/account.rs
Outdated
&self, | ||
worker: &Worker<T>, | ||
receiver_id: AccountId, | ||
amount_yocto: Balance, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be named better. _yocto
perfix seems like a smell (0.9 confidence), and Ballance
feels like a weirdly non-specific name (0.7 confidence, not familiar with domain). I'd expect amount: Near
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Balance
is the type that gets used everywhere for this even such as api-js
. It make sense for it to be balance since you are transferring Balance
s between accounts. The name Near
is already a bit bloated in the ecosystem is the thing. How about I just have it be amount: Balance
? - since Balance
is by default yocto and the expectation would be that a user use one of the utility functions such as pase_near!("10 N")
@@ -40,7 +101,191 @@ impl Contract { | |||
&self.account.id | |||
} | |||
|
|||
pub fn as_account(&self) -> &Account { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe fn owner(&self) -> &Account
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The contract's account is not necessarily the owner though, since the behavior for an owner can belong to the parent account or the account that created this Contract
/Account
workspaces/tests/create_account.rs
Outdated
#[tokio::test] | ||
async fn test_subaccount_creation() -> anyhow::Result<()> { | ||
let worker = workspaces::sandbox(); | ||
let account = worker.dev_create().await?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dev_create
seems like less than ideal name. Dev create what? Maybe worker.test_account()
or worker.new_test_account()
? worker.test_account()
which always returns the same account sounds like a potentially good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so I wanted to tie this with dev_deploy
since they're very similar except dev_deploy
also deploys a contract. How about dev_create_account()
? Returning the same account will be problematic for testnet since that could cause some non-determinism if every testnet test used the same account and modified the accounts state
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning the same account will be problematic for testnet since that could cause some non-determinism if every testnet test used the same account
I mean that "the same" is scoped to the worker
-- basically, "create a new dev account" is something you use rarely, and for most tests, which need just a single account, you use "get the test account", which is created on the first access.
Not sure whether this is a good idea...
Thinking more about this, it's probably a good idea -- for the first release we should aim for orthogonal API, and we can add shortcuts later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potentially horrible idea this is async function, so the user already has to type .await
. So what we can do is
let account_without_code = worker.dev_create_account().await?;
let account_with_code = worker.dev_create_account().with_code(&wasm).await?;
This reads pretty nice at the call-site, but would be quite mind bending in the rustdoc. I wonder if there's some precedent somewhere for such async builders?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean that "the same" is scoped to the worker -- basically, "create a new dev account" is something you use rarely, and for most tests, which need just a single account, you use "get the test account", which is created on the first access.
That pretty much is the root_account
for Sandbox which covers most of those cases. For Testnet we can emulate this by doing what you describe.
Thinking more about this, it's probably a good idea -- for the first release we should aim for orthogonal API, and we can add shortcuts later.
Wait, did you mean bad idea instead of good idea here based on you saying we should keep it separate or orthogonal.
I wonder if there's some precedent somewhere for such async builders?
hmm, yeah's let consider this for the release after the first one I'm making today or tomorrow. Would be interesting if we made the builder await
able as that could replace the transact()
calls
self | ||
} | ||
|
||
pub async fn transact(self) -> anyhow::Result<CallExecutionDetails> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Somewhat awkward name, not sure what would be better .call
perhaps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the rationale here, and just my assumption, is just that transact
makes it more clear that it's signing and sending a transaction rather than just calling a function in a view context, but this might be intuitive for a developer anyway.
self | ||
} | ||
|
||
pub async fn transact(self) -> anyhow::Result<CallExecutionDetails> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the rationale here, and just my assumption, is just that transact
makes it more clear that it's signing and sending a transaction rather than just calling a function in a view context, but this might be intuitive for a developer anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing I will note is that this is missing a lot of docs, we can always make a pass through before releasing to make sure it's all there, but might be better to do some now when you have the context
workspaces/src/network/account.rs
Outdated
@@ -79,6 +79,22 @@ impl Account { | |||
new_account_id, | |||
) | |||
} | |||
|
|||
pub async fn deploy<T: Network>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ChaoticTempest HOW IN THE WORLD ARE YOU THIS FAST
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
¯\_(ツ)_/¯
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, definitely an improvement. The only note I'd make is that the examples it might be more clear if the account signing the transactions are an account other than the contract itself, since this is an uncommon pattern in practice
workspaces/src/network/account.rs
Outdated
pub async fn deploy<T: Network>( | ||
self, | ||
worker: &Worker<T>, | ||
wasm: Vec<u8>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if, like the other conversion from String
to &str
, this should also be done because in the future we can assume that we don't actually require an owned vec to be able to serialize and send over the wire?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the thing about a lot of the near_primitives
/rpc, a lot of it requires owned types. Like DeployContractAction
takes in a Vec<u8>
so I can't do much here unless we change the deps
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important meta note: whatever our private deps do internally should not have any bearing on the public API we expose.
The RPC lib we use can use &[u8]
or Vec<u8>
, or Bytes
, but that shouldn't change the public types. We should be able to refactor implementation from Vec<u8>
to Bytes
and back, if we like to, without making our user's change the code.
In this sense, yeah, it seems that wasm: &[u8]
would be a better signature here -- nothing in the problem fundamentally requires owned vec. I can also imagine the following idiom for sandbox testing:
let CODE: &'static [u8] = include_bytes!("./target/wasm32-unknown-unknow/contrct.wasm");
#[test]
fn foo() {}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, context is the king: in some cases, you want the abstraction to be inherently leaky to have, eg, maximal performance. But what we are doing here is essentially a scripting layer, so we have a lot of freedom in how we organize internals, and have a lot of responsibility in making the public API most natural for the user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, let's just do wasm: AsRef<[u8]>
then so we can take in both Vec<u8>
and &[u8]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using AsRef
in the APIs is generally a bad idea, as it makes the function signature much harder to read, and also regresses type inference at the call-site. If the user have xs: Vec<u8>
, they can just pass &xs
.
The only place where AsRef polymoprhism makes sense is for accepting a &Path
. Note that it is needed there not to convert from owned to borrowed (PathBuf -> Path) but between borrowed types (&str -> &Path). And even than, it creates usability paper-cut, where passing in PathBuf
works, but then you loose owenrship of it and can't, eg, print it in the error message.
gonna pull this one in as it seems pretty robust in its current form, but we can still continue the conversation here |
requires #25 to be merged in first.
addresses one of the bullet points in #28
Account/Contract Convenience Functions
Can call directly into the contract using the contract's signer to sign the transaction itself. Does not require to specify the contract_id
account.transfer_near
,account.delete_account
,account.deploy
andcontract.delete_contract
now available