Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: convenience functions for Contract and Account #33

Merged
merged 23 commits into from
Dec 16, 2021

Conversation

ChaoticTempest
Copy link
Member

@ChaoticTempest ChaoticTempest commented Dec 8, 2021

requires #25 to be merged in first.

addresses one of the bullet points in #28

Account/Contract Convenience Functions

let worker = workspaces::sandbox();
let contract = worker.dev_deploy(...).await?;
let account = worker.dev_create().await?;

account.call(&worker, contract_id, "function".into())
   .with_args( ... )
   .with_gas( /*optional: defaults to an internal constant */ )
   .with_deposit( /* optional: defaults to 0 */)
   .transact()
   .await?;

Can call directly into the contract using the contract's signer to sign the transaction itself. Does not require to specify the contract_id

contract.call(&worker, "function".into())
   .with_args( ... )
   .with_gas( /*optional: defaults to an internal constant */ )
   .with_deposit( /* optional: defaults to 0 */)
   .transact()
   .await?;

account.transfer_near, account.delete_account, account.deploy and contract.delete_contract now available

@ChaoticTempest ChaoticTempest changed the base branch from feat/no-pub-nearcore-types to main December 10, 2021 23:15
@ChaoticTempest ChaoticTempest changed the title feat: convenience Account::call using call builder feat: convenience functions for Contract and Account Dec 10, 2021
@ChaoticTempest ChaoticTempest marked this pull request as ready for review December 10, 2021 23:21
@TrevorJTClarke
Copy link
Contributor

More of a context question:

    let agent = worker.dev_create().await?;
    println!("AGENT: {}", agent.id());

What would the method be for mainnet support? worker.create?
I would expect I should be able to do this, given i have an account that has balance and can create sub-accounts.

@willemneal
Copy link

@ChaoticTempest shouldn't it be account_id to stay consistent with the sdk?

@ChaoticTempest
Copy link
Member Author

@TrevorJTClarke Equivalent in mainnet would be worker.create_tla/worker.create_tla_and_deploy. I haven't exposed sub-account creation yet, but let me add it to this PR too since it's another convenience function for account.

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}"

@willemneal

@ChaoticTempest shouldn't it be account_id to stay consistent with the sdk?

You mean how we reference .id() instead of .account_id()? I think that's only apart of near-sdk-sim so we don't need to make it consistent just quite.

workspaces/src/network/account.rs Outdated Show resolved Hide resolved
workspaces/src/network/account.rs Outdated Show resolved Hide resolved
workspaces/src/network/account.rs Outdated Show resolved Hide resolved
Comment on lines 240 to 241
parent_id: AccountId,
new_account_id: String,
Copy link
Contributor

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?

Copy link
Member Author

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

Copy link
Contributor

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?

Copy link
Member Author

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

Copy link
Contributor

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!

workspaces/src/rpc/client.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@matklad matklad left a 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!

examples/src/spooning.rs Outdated Show resolved Hide resolved
workspaces/src/network/account.rs Outdated Show resolved Hide resolved
&self,
worker: &Worker<T>,
receiver_id: AccountId,
amount_yocto: Balance,
Copy link
Contributor

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.

Copy link
Member Author

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 Balances 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")

workspaces/src/network/account.rs Outdated Show resolved Hide resolved
@@ -40,7 +101,191 @@ impl Contract {
&self.account.id
}

pub fn as_account(&self) -> &Account {
Copy link
Contributor

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

Copy link
Member Author

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/src/network/result.rs Outdated Show resolved Hide resolved
workspaces/src/network/result.rs Outdated Show resolved Hide resolved
#[tokio::test]
async fn test_subaccount_creation() -> anyhow::Result<()> {
let worker = workspaces::sandbox();
let account = worker.dev_create().await?;
Copy link
Contributor

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.

Copy link
Member Author

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

Copy link
Contributor

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.

Copy link
Contributor

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?

Copy link
Member Author

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 awaitable as that could replace the transact() calls

self
}

pub async fn transact(self) -> anyhow::Result<CallExecutionDetails> {
Copy link
Contributor

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?

Copy link
Contributor

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.

workspaces/src/rpc/client.rs Show resolved Hide resolved
workspaces/src/network/account.rs Outdated Show resolved Hide resolved
self
}

pub async fn transact(self) -> anyhow::Result<CallExecutionDetails> {
Copy link
Contributor

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.

workspaces/src/network/result.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@austinabell austinabell left a 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 Show resolved Hide resolved
@@ -79,6 +79,22 @@ impl Account {
new_account_id,
)
}

pub async fn deploy<T: Network>(
Copy link
Contributor

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

Copy link
Member Author

@ChaoticTempest ChaoticTempest Dec 16, 2021

Choose a reason for hiding this comment

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

¯\_(ツ)_/¯

Copy link
Contributor

@austinabell austinabell left a 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/result.rs Outdated Show resolved Hide resolved
pub async fn deploy<T: Network>(
self,
worker: &Worker<T>,
wasm: Vec<u8>,
Copy link
Contributor

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?

Copy link
Member Author

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

Copy link
Contributor

@matklad matklad Dec 16, 2021

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() {}

Copy link
Contributor

@matklad matklad Dec 16, 2021

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.

Copy link
Member Author

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]

Copy link
Contributor

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.

@ChaoticTempest
Copy link
Member Author

gonna pull this one in as it seems pretty robust in its current form, but we can still continue the conversation here

@ChaoticTempest ChaoticTempest merged commit ed74d20 into main Dec 16, 2021
@ChaoticTempest ChaoticTempest deleted the feat/call-builder branch December 16, 2021 21:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants