Skip to content

feat: state-init command#560

Merged
vsavchyn-dev merged 41 commits into
near:mainfrom
pityjllk:feat/state-init
Apr 16, 2026
Merged

feat: state-init command#560
vsavchyn-dev merged 41 commits into
near:mainfrom
pityjllk:feat/state-init

Conversation

@pityjllk

@pityjllk pityjllk commented Feb 27, 2026

Copy link
Copy Markdown
Contributor

Add StateInit support for deterministic account creation (NEP-616)

This PR adds support for creating deterministic accounts via DeterministicAccountStateInit as specified in https://github.com/near/NEPs/blob/master/neps/nep-0616.md.

New command: contract state-init

Creates a deterministic account by providing a state init. The state init can be specified in three ways:

  • use-global-hash — reference a globally deployed contract by its code hash
  • use-global-account-id <account_id> — reference a globally deployed contract by the account that deployed it
  • from-borsh-base64 — provide a pre-serialized DeterministicAccountStateInit directly

For the first two modes, the initialization data (a key-value map with base64-encoded keys and values) is provided separately:

  • data-from-json '' — inline JSON, e.g. data-from-json '{"AAEC": "AwQF"}'
  • data-from-file — read the same JSON format from a file

The receiver account ID is automatically derived from the state init contents (deterministic keccak256-based address with 0s prefix).

Changes to transaction construct-transaction

The receiver is now selected via a ReceiverMode enum:

  • account-id — specify the receiver account ID directly (existing behavior)
  • state-init — derive the receiver from a deterministic state init, using the same three input modes described above

This was achieved by splitting the old ConstructTransactionContext into two stages: ConstructTransactionSenderContext (sender only) → ConstructTransactionContext (sender + receiver +
actions). The receiver selection step sits between the two.

Example usage

Create a deterministic account and deploy via state-init:

  near transaction construct-transaction signer.near \
    state-init use-global-hash FaJXVgS82fXhrvvC8yXV4ibHujW63KvL7dVvJiZ9naga \
    data-from-json '{"":"EAAAAHBpdHlqbGxrLnRlc3RuZXQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANiLmaE3mtdPRW1WVXs6pXbGdg5/4OmhjI6pswq6Nr/j"}' \
    deposit '0 NEAR' \
    add-action function-call gd_deploy \
      file-args /path/to/contract.wasm.borsh \
      prepaid-gas '300.0 Tgas' attached-deposit '14.5 NEAR' \
    skip network-config testnet sign-with-keychain send

Call a function on a deterministic account (receiver specified directly):

  near transaction construct-transaction matdeployer.testnet \
    account-id 0s49b604786a4a44077ef5a450ddf59e90ae3d95d0 \
    add-action function-call gd_approve \
      json-args '{"old_hash":"d88b99a...","new_hash":"d88b99a..."}' \
      prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' \
    skip network-config testnet sign-with-keychain send

The account-id keyword is optional — the old positional syntax still works:

  near transaction construct-transaction matdeployer.testnet \
    0s49b604786a4a44077ef5a450ddf59e90ae3d95d0 \
    add-action function-call gd_approve \
      json-args '{"old_hash":"d88b99a...","new_hash":"d88b99a..."}' \
      prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' \
    skip network-config testnet sign-with-keychain send

Backward compatibility

The old CLI syntax (construct-transaction add-action ...) continues to work — it's treated as the
account-id receiver variant. Snapshot tests verify that both old and new syntaxes produce identical transactions (same hash,
same serialized bytes).

@pityjllk pityjllk requested a review from a team as a code owner February 27, 2026 08:22
@github-project-automation github-project-automation Bot moved this to NEW❗ in DevTools Feb 27, 2026
Comment thread src/common.rs Outdated
Comment thread src/commands/contract/state_init/mod.rs
Comment thread src/commands/contract/state_init/mod.rs Outdated
Comment thread src/commands/contract/state_init/mod.rs Outdated
Comment thread src/commands/contract/state_init/mod.rs Outdated
Comment thread src/commands/contract/state_init/mod.rs Outdated
Comment thread src/commands/transaction/construct_transaction/state_init_receiver/mod.rs Outdated
Comment thread src/commands/contract/state_init/mod.rs
Comment thread src/commands/contract/state_init/mod.rs
Comment thread src/commands/contract/state_init/mod.rs
Comment thread src/commands/transaction/construct_transaction/state_init_receiver/mod.rs Outdated

@mitinarseny mitinarseny left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  1. Can we also add support for adding more actions after StateInit? On it's own StateInit is not that useful, the most interesting stuff is when you combine it in one receipt
near contract state-init ... [[add-action...] skip] sign-as ...
  1. It would be nice if, in interactive mode, after state-init use-global-account-id global.near data-json { ... } I can see the derived 0s... address, because further function calls might require to specify it in args

What do you think?

Comment thread src/commands/contract/state_init/mod.rs
Comment thread src/commands/contract/state_init/mod.rs
Comment thread src/commands/contract/state_init/mod.rs Outdated

@vsavchyn-dev vsavchyn-dev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM! Thanks for contributing!

@vsavchyn-dev

Copy link
Copy Markdown
Contributor

@mitinarseny any other suggestions or things that are needed to be changed?

Pics are just for reference that it works.

Constructing state-init transaction with multiple actions:
image

Just a normal flow:
image

@vsavchyn-dev vsavchyn-dev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From what I see it should work as intended, but I just have one question about Inspect for state-init - not sure if it should be there.

@frol - any other suggestions/remarks?

Comment thread src/commands/contract/state_init/mod.rs
Comment thread src/commands/contract/state_init/mod.rs
Comment thread src/main.rs
Parser::parse_from(cmd)
// Backward compat: old `construct-transaction <sender> <receiver>` syntax
// Insert "account-id" subcommand before the bare receiver account ID.
if let Some(rewritten) = try_rewrite_construct_transaction_args() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am curious if this parsing of new construct-transaction will collide with near-cli-js commands that we try to parse?
From what I see in near-cli-js it shouldn't, but maybe someone can spot an issue

@frol

frol commented Mar 31, 2026

Copy link
Copy Markdown
Collaborator

@vsavchyn-dev Thanks for pining. No additional comments from my side. Thank you for the empathy to the CLI users and aligning the messages / naming / outputs so they look consistent!

@mitinarseny

mitinarseny commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

@pityjllk it might make sense to also add contract state-init ... inspect state-init { borsh | json }, since it will be needed to pass its JSON serialized representation as arguments to function-calls, such as for wallet-contracts here. Also, it might be nice to add a roundtrip counterpart for it: contract state-init { from-json | from-json-file }.

Now I'm thinking if contract state-init from-base64-... and contract state-init ... inspect state-init borsh is any useful actually... Wdyt?

@pityjllk

pityjllk commented Apr 7, 2026

Copy link
Copy Markdown
Contributor Author

i think the argument was that since borsh is natvie serialization used at the protocol level then it feels right to support it, it might be useful for debugging etc. also effort has been made already so i would keep it ....

@mitinarseny mitinarseny left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

few more comments on JSON part

Comment on lines +418 to +423
println!(
"{}",
serde_json::to_string_pretty(&previous_context.state_init).map_err(
|e| color_eyre::eyre::eyre!("Failed to serialize state-init to JSON: {e}")
)?
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe only prettify if stdout is tty?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

actually in the whole repo to_string_pretty is used, so it would be insconsisted, so ideally that could be addressed in separate pr?

Comment on lines +26 to +33
#[strum_discriminants(strum(
message = "from-borsh-base64 - Provide borsh serialized base64 encoded state init"
))]
FromBorshBase64(StateInitFromBorshBase64),
#[strum_discriminants(strum(
message = "from-borsh-file - Read borsh-serialized state init from a file"
))]
FromBorshFile(StateInitFromBorshFile),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe also add from-json[-file] for the sake of roundtrip with near contract state-init ... inspect json?
And same for near transaction construct-transaction receiver.near state-init from-json[-file]?

@mitinarseny mitinarseny left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I hope these are final nits from me, sorry 🙃

Comment thread src/commands/contract/state_init/mod.rs Outdated
Comment on lines +26 to +41
#[strum_discriminants(strum(
message = "from-borsh-base64 - Provide borsh serialized base64 encoded state init"
))]
FromBorshBase64(StateInitFromBorshBase64),
#[strum_discriminants(strum(
message = "from-borsh-file - Read borsh-serialized state init from a file"
))]
FromBorshFile(StateInitFromBorshFile),
#[strum_discriminants(strum(
message = "from-json - Provide JSON-serialized state init inline"
))]
FromJson(StateInitFromJson),
#[strum_discriminants(strum(
message = "from-json-file - Read JSON-serialized state init from a file"
))]
FromJsonFile(StateInitFromJsonFile),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe switch places and put json-related first, since they would be more frequently used in the interactive mode?

Comment thread src/commands/contract/state_init/mod.rs Outdated
Comment on lines +459 to +464
#[strum_discriminants(strum(message = "borsh - Borsh-serialized base64"))]
/// Borsh-serialized base64
Borsh(InspectStateInitBorsh),
#[strum_discriminants(strum(message = "json - JSON-serialized"))]
/// JSON-serialized
Json(InspectStateInitJson),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also here: put json first?

@mitinarseny

Copy link
Copy Markdown
Contributor

Note: before merging it we need to upgrade near-primitives-core to a version which includes near/nearcore#15539

Comment thread src/commands/transaction/construct_transaction/state_init_receiver/mod.rs Outdated
Comment thread src/common.rs Outdated
Comment on lines +960 to +979
if deterministic_account_state_init_v1.data.is_empty() {
ret.push_str(&format!("\n{:>31} {:<13} {{}}", "", "data:"));
} else {
ret.push_str(&format!("\n{:>31} {:<13} {{", "", "data:"));
let last_idx = deterministic_account_state_init_v1.data.len() - 1;
for (i, (key, value)) in deterministic_account_state_init_v1.data.iter().enumerate() {
let comma = if i < last_idx { "," } else { "" };
ret.push_str(&format!(
"\n{:>45} \"{}\" : \"{}\"{}",
"", near_primitives::serialize::to_base64(key), near_primitives::serialize::to_base64(value), comma,
));
}
ret.push_str(&format!("\n{:>31} {:<13} }}", "", ""));
}
ret.push_str(&format!("\n{:>31} {:<13} {}", "", "code:", match deterministic_account_state_init_v1.code {
GlobalContractIdentifier::CodeHash(hash) => {
format!("use global <{hash}> code to deploy from")
format!("hash: {hash}")
}
GlobalContractIdentifier::AccountId(ref account_id) => {
format!("use global <{account_id}> code to deploy from")
format!("account ref: {account_id}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since we've standardized JSON representation of StateInit, then maybe print it as a JSON instead of writing custom human-readable serialization?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

│       -- create deterministic account <0s2de1179d14651ce32c16bca892d2e804057b6fcb>:
│                       deposit     : 0 NEAR
│                       state-init  : {
│                                       "V1": {
│                                         "code": {
│                                           "AccountId": "0s29e346108955b88c2d180a4ba17662b1f2cc1028"
│                                         },
│                                         "data": {
│                                           "": "GAAAAGludGVudHMuc3B1dG5pay1kYW8ubmVhcgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAs4NZ2fFCre1avLgVH/oTQj8p5Mx4pXhGj5Xov8nIgpDI="
│                                         }
│                                       }
│                                     }
│

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

how about printing whole state init as json?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, that's exactly what I was meant, nice!

@mitinarseny mitinarseny left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @pityjllk, LGTM!

@vsavchyn-dev vsavchyn-dev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stamp, thanks for the new command and hard work!

@vsavchyn-dev vsavchyn-dev merged commit 1fe9569 into near:main Apr 16, 2026
13 checks passed
@github-project-automation github-project-automation Bot moved this from NEW❗ to Shipped 🚀 in DevTools Apr 16, 2026
@race-of-sloths

race-of-sloths commented Apr 16, 2026

Copy link
Copy Markdown

@pityjllk Thank you for your contribution! Your pull request is now a part of the Race of Sloths!
Do you want to apply for monthly streak? Get 8+ score for a single PR this month and receive boost for race-of-sloths!

Shows inviting banner with latest news.

Shows profile picture for the author of the PR

Current status: executed
Reviewer Score
@vsavchyn-dev 13

Your contribution is much appreciated with a final score of 13!
You have received 140 (130 base + 10 monthly bonus) Sloth points for this contribution

@vsavchyn-dev received 25 Sloth Points for reviewing and scoring this pull request.

Congratulations @pityjllk! Your PR was highly scored and you completed another monthly streak! To keep your monthly streak make another pull request next month and get 8+ score for it

What is the Race of Sloths

Race of Sloths is a friendly competition where you can participate in challenges and compete with other open-source contributors within your normal workflow

For contributors:

  • Tag @race-of-sloths inside your pull requests
  • Wait for the maintainer to review and score your pull request
  • Check out your position in the Leaderboard
  • Keep weekly and monthly streaks to reach higher positions
  • Boast your contributions with a dynamic picture of your Profile

For maintainers:

  • Score pull requests that participate in the Race of Sloths and receive a reward
  • Engage contributors with fair scoring and fast responses so they keep their streaks
  • Promote the Race to the point where the Race starts promoting you
  • Grow the community of your contributors

Feel free to check our website for additional details!

Bot commands
  • For contributors
    • Include a PR: @race-of-sloths include to enter the Race with your PR
  • For maintainers:
    • Invite contributor @race-of-sloths invite to invite the contributor to participate in a race or include it, if it's already a runner.
    • Assign points: @race-of-sloths score [1/2/3/5/8/13] to award points based on your assessment.
    • Reject this PR: @race-of-sloths exclude to send this PR back to the drawing board.
    • Exclude repo: @race-of-sloths pause to stop bot activity in this repo until @race-of-sloths unpause command is called

@vsavchyn-dev

Copy link
Copy Markdown
Contributor

@race-of-sloths score 13

r-near added a commit that referenced this pull request Apr 16, 2026
Resolve conflicts: keep near-kit types throughout, incorporate new
on_sending_delegate_action_callback from #583, convert state-init
command (#560) from near-primitives to near-kit, adopt near-slip10
from #584.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Shipped 🚀

Development

Successfully merging this pull request may close these issues.

8 participants