Skip to content

Commit b0260a4

Browse files
committed
feat: state-init command
1 parent b067abd commit b0260a4

File tree

3 files changed

+380
-4
lines changed

3 files changed

+380
-4
lines changed

src/commands/contract/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod download_abi;
1111
pub mod download_wasm;
1212
#[cfg(feature = "inspect_contract")]
1313
mod inspect;
14+
pub mod state_init;
1415

1516
#[cfg(feature = "verify_contract")]
1617
mod verify;
@@ -44,6 +45,11 @@ pub enum ContractActions {
4445
))]
4546
/// Add a global contract code
4647
DeployAsGlobal(self::deploy_global::Contract),
48+
#[strum_discriminants(strum(
49+
message = "state-init - Initialize a deterministic account with a global contract and state data"
50+
))]
51+
/// Initialize a deterministic account with a global contract and state data
52+
StateInit(self::state_init::StateInit),
4753
#[strum_discriminants(strum(
4854
message = "inspect - Get a list of available function names"
4955
))]
Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
use color_eyre::eyre::Context;
2+
use strum::{EnumDiscriminants, EnumIter, EnumMessage};
3+
4+
#[derive(Debug, Clone, interactive_clap::InteractiveClap)]
5+
#[interactive_clap(context = crate::GlobalContext)]
6+
pub struct StateInit {
7+
#[interactive_clap(subcommand)]
8+
state_init: StateInitModeCommand,
9+
}
10+
11+
#[derive(Debug, EnumDiscriminants, Clone, interactive_clap::InteractiveClap)]
12+
#[interactive_clap(context = crate::GlobalContext)]
13+
#[strum_discriminants(derive(EnumMessage, EnumIter))]
14+
#[non_exhaustive]
15+
/// How do you want to identify the global contract code?
16+
pub enum StateInitModeCommand {
17+
#[strum_discriminants(strum(
18+
message = "use-global-hash - Use a global contract code hash (immutable)"
19+
))]
20+
/// Use a global contract code hash (immutable)
21+
UseGlobalHash(StateInitWithContractHashRef),
22+
#[strum_discriminants(strum(
23+
message = "use-global-account-id - Use a global contract account ID (mutable)"
24+
))]
25+
/// Use a global contract account ID (mutable)
26+
UseGlobalAccountId(StateInitWithContractRefByAccount),
27+
}
28+
29+
#[derive(Debug, Clone, interactive_clap::InteractiveClap)]
30+
#[interactive_clap(input_context = crate::GlobalContext)]
31+
#[interactive_clap(output_context = StateInitWithContractHashRefContext)]
32+
pub struct StateInitWithContractHashRef {
33+
/// What is the hash of the global contract?
34+
pub hash: crate::types::crypto_hash::CryptoHash,
35+
#[interactive_clap(subcommand)]
36+
data: Data,
37+
}
38+
39+
#[derive(Debug, Clone, interactive_clap::InteractiveClap)]
40+
#[interactive_clap(input_context = crate::GlobalContext)]
41+
#[interactive_clap(output_context = StateInitWithContractRefByAccountContext)]
42+
pub struct StateInitWithContractRefByAccount {
43+
/// What is the account ID of the global contract?
44+
pub account_id: crate::types::account_id::AccountId,
45+
#[interactive_clap(subcommand)]
46+
data: Data,
47+
}
48+
49+
#[derive(Debug, Clone)]
50+
pub struct StateInitModeContext {
51+
pub global_context: crate::GlobalContext,
52+
pub code: near_primitives::action::GlobalContractIdentifier,
53+
}
54+
55+
#[derive(Debug, Clone)]
56+
pub struct StateInitWithContractHashRefContext(StateInitModeContext);
57+
58+
impl StateInitWithContractHashRefContext {
59+
pub fn from_previous_context(
60+
previous_context: crate::GlobalContext,
61+
scope: &<StateInitWithContractHashRef as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope,
62+
) -> color_eyre::eyre::Result<Self> {
63+
Ok(Self(StateInitModeContext {
64+
global_context: previous_context,
65+
code: near_primitives::action::GlobalContractIdentifier::CodeHash(scope.hash.into()),
66+
}))
67+
}
68+
}
69+
70+
impl From<StateInitWithContractHashRefContext> for StateInitModeContext {
71+
fn from(item: StateInitWithContractHashRefContext) -> Self {
72+
item.0
73+
}
74+
}
75+
76+
impl From<StateInitWithContractRefByAccountContext> for StateInitModeContext {
77+
fn from(item: StateInitWithContractRefByAccountContext) -> Self {
78+
item.0
79+
}
80+
}
81+
82+
#[derive(Debug, Clone)]
83+
pub struct StateInitWithContractRefByAccountContext(StateInitModeContext);
84+
85+
impl StateInitWithContractRefByAccountContext {
86+
pub fn from_previous_context(
87+
previous_context: crate::GlobalContext,
88+
scope: &<StateInitWithContractRefByAccount as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope,
89+
) -> color_eyre::eyre::Result<Self> {
90+
Ok(Self(StateInitModeContext {
91+
global_context: previous_context,
92+
code: near_primitives::action::GlobalContractIdentifier::AccountId(
93+
scope.account_id.clone().into(),
94+
),
95+
}))
96+
}
97+
}
98+
99+
#[derive(Debug, EnumDiscriminants, Clone, interactive_clap::InteractiveClap)]
100+
#[interactive_clap(context = StateInitModeContext)]
101+
#[strum_discriminants(derive(EnumMessage, EnumIter))]
102+
#[non_exhaustive]
103+
/// How do you want to provide the initial state data?
104+
pub enum Data {
105+
#[strum_discriminants(strum(
106+
message = "data-from-file - Read hex-encoded key-value JSON data from a file"
107+
))]
108+
/// Read hex-encoded key-value JSON data from a file
109+
DataFromFile(DataFromFile),
110+
#[strum_discriminants(strum(
111+
message = "data-from-json - Provide hex-encoded key-value JSON data inline"
112+
))]
113+
/// Provide hex-encoded key-value JSON data inline
114+
DataFromJson(DataFromJson),
115+
}
116+
117+
#[derive(Debug, Clone, interactive_clap::InteractiveClap)]
118+
#[interactive_clap(input_context = StateInitModeContext)]
119+
#[interactive_clap(output_context = DataFromFileContext)]
120+
pub struct DataFromFile {
121+
/// What is the file path of the JSON state data?
122+
pub file_path: crate::types::path_buf::PathBuf,
123+
#[interactive_clap(named_arg)]
124+
/// Specify deposit
125+
deposit: Deposit,
126+
}
127+
128+
#[derive(Debug, Clone, interactive_clap::InteractiveClap)]
129+
#[interactive_clap(input_context = StateInitModeContext)]
130+
#[interactive_clap(output_context = DataFromJsonContext)]
131+
pub struct DataFromJson {
132+
/// Enter hex-encoded key-value JSON data (e.g. '{"deadbeef": "cafebabe"}' or '{}' for empty state):
133+
pub data: String,
134+
#[interactive_clap(named_arg)]
135+
/// Specify deposit
136+
deposit: Deposit,
137+
}
138+
139+
#[derive(Debug, Clone)]
140+
pub struct StateInitDataContext {
141+
pub global_context: crate::GlobalContext,
142+
pub state_init: near_primitives::deterministic_account_id::DeterministicAccountStateInit,
143+
pub receiver_account_id: near_primitives::types::AccountId,
144+
}
145+
146+
impl StateInitDataContext {
147+
fn build(
148+
code: near_primitives::action::GlobalContractIdentifier,
149+
global_context: crate::GlobalContext,
150+
data: std::collections::BTreeMap<Vec<u8>, Vec<u8>>,
151+
) -> color_eyre::eyre::Result<Self> {
152+
let state_init =
153+
near_primitives::deterministic_account_id::DeterministicAccountStateInit::V1(
154+
near_primitives::deterministic_account_id::DeterministicAccountStateInitV1 {
155+
code,
156+
data,
157+
},
158+
);
159+
let receiver_account_id =
160+
near_primitives::utils::derive_near_deterministic_account_id(&state_init);
161+
Ok(Self {
162+
global_context,
163+
state_init,
164+
receiver_account_id,
165+
})
166+
}
167+
}
168+
169+
impl From<DataFromFileContext> for StateInitDataContext {
170+
fn from(item: DataFromFileContext) -> Self {
171+
item.0
172+
}
173+
}
174+
175+
#[derive(Debug, Clone)]
176+
pub struct DataFromFileContext(StateInitDataContext);
177+
178+
impl DataFromFileContext {
179+
pub fn from_previous_context(
180+
previous_context: StateInitModeContext,
181+
scope: &<DataFromFile as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope,
182+
) -> color_eyre::eyre::Result<Self> {
183+
let json_str = std::fs::read_to_string(&scope.file_path).wrap_err_with(|| {
184+
format!(
185+
"Failed to open or read the file: {}",
186+
scope.file_path.0.display()
187+
)
188+
})?;
189+
let data = crate::common::parse_hex_kv_map(&json_str)?;
190+
Ok(Self(StateInitDataContext::build(
191+
previous_context.code,
192+
previous_context.global_context,
193+
data,
194+
)?))
195+
}
196+
}
197+
198+
impl From<DataFromJsonContext> for StateInitDataContext {
199+
fn from(item: DataFromJsonContext) -> Self {
200+
item.0
201+
}
202+
}
203+
204+
#[derive(Debug, Clone)]
205+
pub struct DataFromJsonContext(StateInitDataContext);
206+
207+
impl DataFromJsonContext {
208+
pub fn from_previous_context(
209+
previous_context: StateInitModeContext,
210+
scope: &<DataFromJson as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope,
211+
) -> color_eyre::eyre::Result<Self> {
212+
let data = crate::common::parse_hex_kv_map(&scope.data)?;
213+
Ok(Self(StateInitDataContext::build(
214+
previous_context.code,
215+
previous_context.global_context,
216+
data,
217+
)?))
218+
}
219+
}
220+
221+
#[derive(Debug, Clone, interactive_clap::InteractiveClap)]
222+
#[interactive_clap(input_context = StateInitDataContext)]
223+
#[interactive_clap(output_context = DepositContext)]
224+
pub struct Deposit {
225+
/// How much do you want to deposit with the state init (e.g. '1 NEAR' or '0 NEAR')?
226+
pub deposit: crate::types::near_token::NearToken,
227+
#[interactive_clap(skip_default_input_arg)]
228+
/// What is the signer account ID?
229+
pub signer_account_id: crate::types::account_id::AccountId,
230+
#[interactive_clap(named_arg)]
231+
/// Select network
232+
network_config: crate::network_for_transaction::NetworkForTransactionArgs,
233+
}
234+
235+
impl Deposit {
236+
pub fn input_signer_account_id(
237+
context: &StateInitDataContext,
238+
) -> color_eyre::eyre::Result<Option<crate::types::account_id::AccountId>> {
239+
crate::common::input_signer_account_id_from_used_account_list(
240+
&context.global_context.config.credentials_home_dir,
241+
"What is the signer account ID?",
242+
)
243+
}
244+
}
245+
246+
#[derive(Debug, Clone)]
247+
pub struct DepositContext {
248+
pub global_context: crate::GlobalContext,
249+
pub state_init: near_primitives::deterministic_account_id::DeterministicAccountStateInit,
250+
pub receiver_account_id: near_primitives::types::AccountId,
251+
pub deposit: near_token::NearToken,
252+
pub signer_account_id: near_primitives::types::AccountId,
253+
}
254+
255+
impl DepositContext {
256+
pub fn from_previous_context(
257+
previous_context: StateInitDataContext,
258+
scope: &<Deposit as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope,
259+
) -> color_eyre::eyre::Result<Self> {
260+
Ok(Self {
261+
global_context: previous_context.global_context,
262+
state_init: previous_context.state_init,
263+
receiver_account_id: previous_context.receiver_account_id,
264+
deposit: scope.deposit.into(),
265+
signer_account_id: scope.signer_account_id.clone().into(),
266+
})
267+
}
268+
}
269+
270+
impl From<DepositContext> for crate::commands::ActionContext {
271+
fn from(item: DepositContext) -> Self {
272+
let signer_id = item.signer_account_id.clone();
273+
let receiver_id = item.receiver_account_id.clone();
274+
275+
let get_prepopulated_transaction_after_getting_network_callback: crate::commands::GetPrepopulatedTransactionAfterGettingNetworkCallback =
276+
std::sync::Arc::new({
277+
move |network_config| {
278+
use crate::common::JsonRpcClientExt as _;
279+
let receiver_id = &item.receiver_account_id;
280+
let result = network_config
281+
.json_rpc_client()
282+
.blocking_call_view_account(
283+
receiver_id,
284+
near_primitives::types::Finality::Final.into(),
285+
);
286+
// Best-effort check — only cancel if we positively confirm account exists.
287+
// All errors (UnknownAccount, network timeout, connection refused, etc.)
288+
// are treated as "proceed" to support the sign-later offline signing flow,
289+
// where the network may be unreachable at transaction construction time.
290+
if result.is_ok() {
291+
eprintln!(
292+
"\nDeterministic account <{}> already exists on <{}> network. No transaction needed.",
293+
receiver_id,
294+
network_config.network_name,
295+
);
296+
return Ok(crate::commands::PrepopulatedTransaction {
297+
signer_id: item.signer_account_id.clone(),
298+
receiver_id: item.receiver_account_id.clone(),
299+
actions: vec![],
300+
});
301+
}
302+
Ok(crate::commands::PrepopulatedTransaction {
303+
signer_id: item.signer_account_id.clone(),
304+
receiver_id: item.receiver_account_id.clone(),
305+
actions: vec![
306+
near_primitives::transaction::Action::DeterministicStateInit(Box::new(
307+
near_primitives::action::DeterministicStateInitAction {
308+
state_init: item.state_init.clone(),
309+
deposit: item.deposit,
310+
},
311+
)),
312+
],
313+
})
314+
}
315+
});
316+
317+
Self {
318+
global_context: item.global_context,
319+
interacting_with_account_ids: vec![signer_id, receiver_id],
320+
get_prepopulated_transaction_after_getting_network_callback,
321+
on_before_signing_callback: std::sync::Arc::new(
322+
|_prepopulated_unsigned_transaction, _network_config| Ok(()),
323+
),
324+
on_before_sending_transaction_callback: std::sync::Arc::new(
325+
|_signed_transaction, _network_config| Ok(String::new()),
326+
),
327+
on_after_sending_transaction_callback: std::sync::Arc::new(
328+
|_outcome_view, _network_config| Ok(()),
329+
),
330+
}
331+
}
332+
}

0 commit comments

Comments
 (0)