diff --git a/protobufs/Cargo.toml b/protobufs/Cargo.toml index 58dd61c7..1c043980 100644 --- a/protobufs/Cargo.toml +++ b/protobufs/Cargo.toml @@ -18,12 +18,15 @@ version = "0.12.4" default-features = false features = ["std", "prost-derive"] -# todo: get the tonic devs to actually make `channel` usable without `transport` (it *should*, it's *documented* as such, but it just doesn't work). -[dependencies.tonic] -version = "0.11.0" - [build-dependencies] anyhow = "1.0.55" -tonic-build = "0.11.0" +prost-build = "0.12.4" regex = "1.10.6" fs_extra = "1.3.0" + +[dev-dependencies] +wasm-bindgen-test = "0.3" +wasm-bindgen = "0.2" + +[lib] +crate-type = ["cdylib", "rlib"] diff --git a/protobufs/build.rs b/protobufs/build.rs index bdabcc7d..f8013f67 100644 --- a/protobufs/build.rs +++ b/protobufs/build.rs @@ -18,398 +18,105 @@ * ‍ */ -use std::env; -use std::fs::{ - self, - create_dir_all, - read_dir, +use std::{ + env, + fs, + path::{Path, PathBuf}, }; -use std::path::Path; -use regex::RegexBuilder; +fn main() -> Result<(), Box> { + let out_dir = PathBuf::from(env::var("OUT_DIR")?); + let temp_dir = out_dir.join("proto_temp"); + let google_dir = temp_dir.join("google/protobuf"); -const DERIVE_EQ_HASH: &str = "#[derive(Eq, Hash)]"; -const DERIVE_EQ_HASH_COPY: &str = "#[derive(Copy, Eq, Hash)]"; -const SERVICES_FOLDER: &str = "./protobufs/services"; -const EVENT_FOLDER: &str = "./protobufs/platform/event"; - -fn main() -> anyhow::Result<()> { - // services is the "base" module for the hedera protobufs - // in the beginning, there was only services and it was named "protos" - - let services_path = Path::new(SERVICES_FOLDER); - - // The contents of this folder will be copied and modified before it is - // used for code generation. Later we will suppress generation of cargo - // directives on the copy, so set a directive on the source. - println!("cargo:rerun-if-changed={}", SERVICES_FOLDER); - - if !services_path.is_dir() { - anyhow::bail!("Folder {SERVICES_FOLDER} does not exist; do you need to `git submodule update --init`?"); + // Clean and create directories + if temp_dir.exists() { + fs::remove_dir_all(&temp_dir)?; } + fs::create_dir_all(&google_dir)?; - let out_dir = env::var("OUT_DIR")?; - let out_path = Path::new(&out_dir); - let services_tmp_path = out_path.join("services_src"); - - // ensure we start fresh - let _ = fs::remove_dir_all(&services_tmp_path); - - create_dir_all(&services_tmp_path)?; + let cwd = env::current_dir()?; + let proto_root = cwd.join("protobufs"); + let services_dir = proto_root.join("services"); + let platform_dir = proto_root.join("platform"); - // copy over services into our tmp path so we can edit - fs_extra::copy_items( - &[services_path], - &out_path, - &fs_extra::dir::CopyOptions::new().overwrite(true).copy_inside(false), - )?; - fs::rename(out_path.join("services"), &services_tmp_path)?; - - let event_path = Path::new(EVENT_FOLDER); - println!("cargo:rerun-if-changed={}", EVENT_FOLDER); - - if !event_path.is_dir() { - anyhow::bail!( - "Folder {EVENT_FOLDER} does not exist; do you need to `git submodule update --init`?" - ); + // Copy service and platform protos to temp directory + if services_dir.exists() { + fs::create_dir_all(&temp_dir)?; + copy_proto_files(&services_dir, &temp_dir)?; } - - let event_tmp_path = out_path.join("event"); - - // // Ensure we start fresh - let _ = fs::remove_dir_all(&event_tmp_path); - - create_dir_all(&event_tmp_path)?; - - // Copy the event folder - fs_extra::copy_items( - &[event_path], - &services_tmp_path, - &fs_extra::dir::CopyOptions::new().overwrite(true).copy_inside(false), - )?; - fs::rename(out_path.join("event"), &event_tmp_path)?; - let _ = fs::remove_dir_all(&event_tmp_path); - - let services: Vec<_> = read_dir(&services_tmp_path)? - .chain(read_dir(&services_tmp_path.join("auxiliary").join("tss"))?) - .chain(read_dir(&services_tmp_path.join("event"))?) - .filter_map(|entry| { - let entry = entry.ok()?; - - entry.file_type().ok()?.is_file().then(|| entry.path()) - }) - .collect(); - - // iterate through each file - let re_package = RegexBuilder::new(r"^package (.*);$").multi_line(true).build()?; - for service in &services { - let contents = fs::read_to_string(service)?; - - // ensure that every `package _` entry is `package proto;` - let contents = re_package.replace(&contents, "package proto;"); - - // remove com.hedera.hapi.node.addressbook. prefix - let contents = contents.replace("com.hedera.hapi.node.addressbook.", ""); - - // remove com.hedera.hapi.services.auxiliary.tss. prefix - let contents = contents.replace("com.hedera.hapi.services.auxiliary.tss.", ""); - - // remove com.hedera.hapi.platform.event. prefix - let contents = contents.replace("com.hedera.hapi.platform.event.", ""); - - fs::write(service, &*contents)?; + if platform_dir.exists() { + fs::create_dir_all(&temp_dir)?; + copy_proto_files(&platform_dir, &temp_dir)?; } - let mut cfg = tonic_build::configure() - // We have already emitted a cargo directive to trigger a rerun on the source folder - // that the copy this builds is based on. If the directives are not suppressed, the - // crate will rebuild on every compile due to the modified time stamps post-dating - // the start time of the compile action. - .emit_rerun_if_changed(false); - - // most of the protobufs in "basic types" should be Eq + Hash + Copy - // any protobufs that would typically be used as parameter, that meet the requirements of those - // traits - cfg = cfg - .type_attribute("proto.ShardID", DERIVE_EQ_HASH_COPY) - .type_attribute("proto.RealmID", DERIVE_EQ_HASH_COPY) - .type_attribute("proto.AccountID", DERIVE_EQ_HASH) - .type_attribute("proto.AccountID.account", DERIVE_EQ_HASH) - .type_attribute("proto.FileID", DERIVE_EQ_HASH_COPY) - .type_attribute("proto.ContractID", DERIVE_EQ_HASH) - .type_attribute("proto.ContractID.contract", DERIVE_EQ_HASH) - .type_attribute("proto.TransactionID", DERIVE_EQ_HASH) - .type_attribute("proto.Timestamp", DERIVE_EQ_HASH_COPY) - .type_attribute("proto.NftTransfer", DERIVE_EQ_HASH) - .type_attribute("proto.Fraction", DERIVE_EQ_HASH_COPY) - .type_attribute("proto.TopicID", DERIVE_EQ_HASH_COPY) - .type_attribute("proto.TokenID", DERIVE_EQ_HASH_COPY) - .type_attribute("proto.ScheduleID", DERIVE_EQ_HASH_COPY) - .type_attribute("proto.FeeComponents", DERIVE_EQ_HASH_COPY) - .type_attribute("proto.Key", DERIVE_EQ_HASH) - .type_attribute("proto.KeyList", DERIVE_EQ_HASH) - .type_attribute("proto.ThresholdKey", DERIVE_EQ_HASH) - .type_attribute("proto.Key.key", DERIVE_EQ_HASH) - .type_attribute("proto.SignaturePair", DERIVE_EQ_HASH) - .type_attribute("proto.SignaturePair.signature", DERIVE_EQ_HASH) - .type_attribute("proto.FeeData", DERIVE_EQ_HASH_COPY) - .type_attribute("proto.TokenBalance", DERIVE_EQ_HASH_COPY) - .type_attribute("proto.TokenAssociation", DERIVE_EQ_HASH) - .type_attribute("proto.CryptoAllowance", DERIVE_EQ_HASH) - .type_attribute("proto.TokenAllowance", DERIVE_EQ_HASH) - .type_attribute("proto.GrantedCryptoAllowance", DERIVE_EQ_HASH) - .type_attribute("proto.GrantedTokenAllowance", DERIVE_EQ_HASH) - .type_attribute("proto.Duration", DERIVE_EQ_HASH_COPY); - - // the ResponseCodeEnum should be marked as #[non_exhaustive] so - // adding variants does not trigger a breaking change - cfg = cfg.type_attribute("proto.ResponseCodeEnum", "#[non_exhaustive]"); - - // the ResponseCodeEnum is not documented in the proto source - cfg = cfg.type_attribute( - "proto.ResponseCodeEnum", - r#"#[doc = " - Returned in `TransactionReceipt`, `Error::PreCheckStatus`, and `Error::ReceiptStatus`. - - The success variant is `Success` which is what a `TransactionReceipt` will contain for a - successful transaction. - "]"#, - ); + println!("cargo:rerun-if-changed={}", services_dir.display()); + println!("cargo:rerun-if-changed={}", platform_dir.display()); - // Services fails with message: - // --- stderr - // Error: protoc failed: event/state_signature_transaction.proto: File not found. - // transaction_body.proto:111:1: Import "event/state_signature_transaction.proto" was not found or had errors. - // - cfg.compile(&services, &[services_tmp_path])?; + let mut protos = Vec::new(); + collect_proto_files(&temp_dir, &mut protos)?; - // panic!("Services succeeded"); + let mut config = prost_build::Config::new(); + config.protoc_arg("--experimental_allow_proto3_optional"); + config.out_dir(&temp_dir); + config.include_file("mod.rs"); - // NOTE: prost generates rust doc comments and fails to remove the leading * line - remove_useless_comments(&Path::new(&env::var("OUT_DIR")?).join("proto.rs"))?; - - // mirror - // NOTE: must be compiled in a separate folder otherwise it will overwrite the previous build - - let mirror_out_dir = Path::new(&env::var("OUT_DIR")?).join("mirror"); - create_dir_all(&mirror_out_dir)?; - - tonic_build::configure() - .build_server(false) - .extern_path(".proto.Timestamp", "crate::services::Timestamp") - .extern_path(".proto.TopicID", "crate::services::TopicId") - .extern_path(".proto.FileID", "crate::services::FileId") - .extern_path(".proto.NodeAddress", "crate::services::NodeAddress") - .extern_path( - ".proto.ConsensusMessageChunkInfo", - "crate::services::ConsensusMessageChunkInfo", - ) - .out_dir(&mirror_out_dir) - .compile( - &[ - "./protobufs/mirror/consensus_service.proto", - "./protobufs/mirror/mirror_network_service.proto", - ], - &["./protobufs/mirror/", "./protobufs/services/"], - )?; - - remove_useless_comments(&mirror_out_dir.join("proto.rs"))?; - - // streams - // NOTE: must be compiled in a separate folder otherwise it will overwrite the previous build - - let streams_out_dir = Path::new(&env::var("OUT_DIR")?).join("streams"); - create_dir_all(&streams_out_dir)?; - - // NOTE: **ALL** protobufs defined in basic_types must be specified here - let cfg = tonic_build::configure(); - let cfg = builder::extern_basic_types(cfg); - - cfg.out_dir(&streams_out_dir).compile( - &["./protobufs/streams/account_balance_file.proto"], - &["./protobufs/streams/", "./protobufs/services/"], - )?; - - // see note wrt services. - remove_useless_comments(&streams_out_dir.join("proto.rs"))?; - - // sdk - // NOTE: must be compiled in a separate folder otherwise it will overwrite the previous build - let sdk_out_dir = Path::new(&env::var("OUT_DIR")?).join("sdk"); - create_dir_all(&sdk_out_dir)?; - - // note: - // almost everything in services must be specified here. - let cfg = tonic_build::configure(); - let cfg = builder::extern_basic_types(cfg) - .services_same("AssessedCustomFee") - .services_same("ConsensusCreateTopicTransactionBody") - .services_same("ConsensusDeleteTopicTransactionBody") - .services_same("ConsensusMessageChunkInfo") - .services_same("ConsensusSubmitMessageTransactionBody") - .services_same("ConsensusUpdateTopicTransactionBody") - .services_same("ContractCallTransactionBody") - .services_same("ContractCreateTransactionBody") - .services_same("ContractDeleteTransactionBody") - .services_same("ContractUpdateTransactionBody") - .services_same("CryptoAddLiveHashTransactionBody") - .services_same("CryptoApproveAllowanceTransactionBody") - .services_same("CryptoCreateTransactionBody") - .services_same("CryptoDeleteTransactionBody") - .services_same("CryptoDeleteAllowanceTransactionBody") - .services_same("CryptoTransferTransactionBody") - .services_same("CryptoUpdateTransactionBody") - .services_same("CryptoDeleteLiveHashTransactionBody") - .services_same("CustomFee") - .services_same("Duration") - .services_same("EthereumTransactionBody") - .services_same("FileAppendTransactionBody") - .services_same("FileCreateTransactionBody") - .services_same("FileDeleteTransactionBody") - .services_same("FileUpdateTransactionBody") - .services_same("FixedFee") - .services_same("FractionalFee") - .services_same("FreezeTransactionBody") - .services_same("FreezeType") - .services_same("LiveHash") - .services_same("NftRemoveAllowance") - .services_same("NodeStake") - .services_same("NodeStakeUpdateTransactionBody") - .services_same("RoyaltyFee") - .services_same("SchedulableTransactionBody") - .services_same("ScheduleCreateTransactionBody") - .services_same("ScheduleDeleteTransactionBody") - .services_same("ScheduleSignTransactionBody") - .services_same("SystemDeleteTransactionBody") - .services_same("SystemUndeleteTransactionBody") - .services_same("TokenAssociateTransactionBody") - .services_same("TokenBurnTransactionBody") - .services_same("TokenCreateTransactionBody") - .services_same("TokenDeleteTransactionBody") - .services_same("TokenDissociateTransactionBody") - .services_same("TokenFeeScheduleUpdateTransactionBody") - .services_same("TokenFreezeAccountTransactionBody") - .services_same("TokenGrantKycTransactionBody") - .services_same("TokenMintTransactionBody") - .services_same("TokenPauseTransactionBody") - .services_same("TokenRevokeKycTransactionBody") - .services_same("TokenUnfreezeAccountTransactionBody") - .services_same("TokenUnpauseTransactionBody") - .services_same("TokenUpdateTransactionBody") - .services_same("TokenUpdateNftsTransactionBody") - .services_same("TokenWipeAccountTransactionBody") - .services_same("TssMessageTransactionBody") - .services_same("TssVoteTransactionBody") - .services_same("TssShareSignatureTransactionBody") - .services_same("TssEncryptionKeyTransactionBody") - .services_same("Transaction") - .services_same("TransactionBody") - .services_same("UncheckedSubmitBody") - .services_same("UtilPrngTransactionBody") - .services_same("VirtualAddress"); - - cfg.out_dir(&sdk_out_dir).compile( - &["./protobufs/sdk/transaction_list.proto"], - &["./protobufs/sdk/", "./protobufs/services/"], - )?; - - // see note wrt services. - remove_useless_comments(&sdk_out_dir.join("proto.rs"))?; + let google_parent = google_dir.parent().unwrap().to_path_buf(); + config.compile_protos(&protos, &[&temp_dir, &google_parent])?; Ok(()) } -fn remove_useless_comments(path: &Path) -> anyhow::Result<()> { - let mut contents = fs::read_to_string(path)?; - - contents = contents.replace("///*\n", ""); - contents = contents.replace("/// *\n", ""); - contents = contents.replace("/// UNDOCUMENTED", ""); - - fs::write(path, contents)?; - +fn copy_proto_files(src_dir: &Path, dst_dir: &Path) -> Result<(), Box> { + for entry in fs::read_dir(src_dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_file() && path.extension().map_or(false, |ext| ext == "proto") { + let content = fs::read_to_string(&path)?; + let patched_content = patch_proto_content(&content)?; + let dst_path = dst_dir.join(path.file_name().unwrap()); + fs::write(dst_path, patched_content)?; + } else if path.is_dir() { + let dst_subdir = dst_dir.join(path.file_name().unwrap()); + fs::create_dir_all(&dst_subdir)?; + copy_proto_files(&path, &dst_subdir)?; + } + } Ok(()) } -trait BuilderExtensions { - fn services_path, U: AsRef>(self, proto_name: T, rust_name: U) -> Self - where - Self: Sized; - - fn services_same>(self, name: T) -> Self - where - Self: Sized, - { - self.services_path(&name, &name) +fn collect_proto_files(dir: &Path, protos: &mut Vec) -> Result<(), Box> { + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_file() && path.extension().map_or(false, |ext| ext == "proto") { + protos.push(path); + } else if path.is_dir() { + collect_proto_files(&path, protos)?; + } } + Ok(()) } -impl BuilderExtensions for tonic_build::Builder { - fn services_path, U: AsRef>(self, proto_name: T, rust_name: U) -> Self { - let proto_name = proto_name.as_ref(); - let rust_name = rust_name.as_ref(); - - self.extern_path(format!(".proto.{proto_name}"), format!("crate::services::{rust_name}")) +fn patch_proto_content(content: &str) -> Result> { + let mut lines: Vec = content.lines().map(|s| s.to_string()).collect(); + + // Find all import lines + let mut import_lines = Vec::new(); + for (i, line) in lines.iter().enumerate() { + if line.trim().starts_with("import") { + import_lines.push((i, line.clone())); + } } -} -mod builder { - use crate::BuilderExtensions; - - pub(super) fn extern_basic_types(builder: tonic_build::Builder) -> tonic_build::Builder { - builder - .services_same("Fraction") - .services_same("Timestamp") - .services_path("AccountID", "AccountId") - .services_path("TokenID", "TokenId") - .services_same("AccountAmount") - .services_same("CurrentAndNextFeeSchedule") - .services_same("FeeComponents") - .services_same("FeeData") - .services_same("FeeSchedule") - .services_same("Key") - .services_path("FileID", "FileId") - .services_same("KeyList") - .services_same("NftTransfer") - .services_same("NodeAddress") - .services_same("NodeAddressBook") - .services_path("RealmID", "RealmId") - .services_path("ScheduleID", "ScheduleId") - .services_path("SemanticVersion", "SemanticVersion") - .services_path("ServiceEndpoint", "ServiceEndpoint") - .services_same("ServicesConfigurationList") - .services_path("Setting", "Setting") - .services_path("ShardID", "ShardId") - .services_path("Signature", "Signature") - .services_path("SignatureList", "SignatureList") - .services_path("SignatureMap", "SignatureMap") - .services_path("SignaturePair", "SignaturePair") - .services_path("ThresholdKey", "ThresholdKey") - .services_path("ThresholdSignature", "ThresholdSignature") - .services_path("TimestampSeconds", "TimestampSeconds") - .services_path("TokenBalance", "TokenBalance") - .services_path("TokenBalances", "TokenBalances") - .services_path("TokenRelationship", "TokenRelationship") - .services_path("TokenTransferList", "TokenTransferList") - .services_path("TopicID", "TopicId") - .services_path("TransactionFeeSchedule", "TransactionFeeSchedule") - .services_path("TransactionID", "TransactionId") - .services_path("TransferList", "TransferList") - .services_path("HederaFunctionality", "HederaFunctionality") - .services_path("SubType", "SubType") - .services_path("TokenFreezeStatus", "TokenFreezeStatus") - .services_path("TokenKycStatus", "TokenKycStatus") - .services_path("TokenSupplyType", "TokenSupplyType") - .services_path("TokenType", "TokenType") - .services_path("GrantedCryptoAllowance", "GrantedCryptoAllowance") - .services_path("GrantedTokenAllowance", "GrantedTokenAllowance") - .services_path("CryptoAllowance", "CryptoAllowance") - .services_path("TokenAllowance", "TokenAllowance") - .services_path("GrantedNftAllowance", "GrantedNftAllowance") - .services_path("NftAllowance", "NftAllowance") - .services_path("TokenPauseStatus", "TokenPauseStatus") - .services_path("TokenAssociation", "TokenAssociation") - .services_path("ContractID", "ContractId") - .services_path("StakingInfo", "StakingInfo") + // Update imports to use correct paths + for (i, line) in import_lines { + if line.contains("\"google/protobuf/") { + lines[i] = line.replace("\"google/protobuf/", "\"google/protobuf/"); + } else { + lines[i] = line.replace("\"", "\""); + } } + + Ok(lines.join("\n")) } diff --git a/protobufs/src/lib.rs b/protobufs/src/lib.rs index a3852007..c8f7454e 100644 --- a/protobufs/src/lib.rs +++ b/protobufs/src/lib.rs @@ -18,35 +18,15 @@ * ‍ */ +#![allow(clippy::large_enum_variant)] +#![allow(clippy::derive_partial_eq_without_eq)] +#![allow(clippy::redundant_closure)] #![allow(non_camel_case_types)] #![allow(clippy::default_trait_access, clippy::doc_markdown)] -#[cfg(feature = "time_0_3")] -mod time_0_3; - -#[cfg(feature = "fraction")] -mod fraction; - -// fixme: Do this, just, don't warn 70 times in generated code. #[allow(clippy::derive_partial_eq_without_eq)] pub mod services { - tonic::include_proto!("proto"); -} - -// fixme: Do this, just, don't warn 70 times in generated code. -#[allow(clippy::derive_partial_eq_without_eq)] -pub mod mirror { - tonic::include_proto!("mirror/com.hedera.mirror.api.proto"); + include!(concat!(env!("OUT_DIR"), "/proto_temp/mod.rs")); } -// fixme: Do this, just, don't warn 70 times in generated code. -#[allow(clippy::derive_partial_eq_without_eq)] -pub mod streams { - tonic::include_proto!("streams/proto"); -} - -// fixme: Do this, just, don't warn 70 times in generated code. -#[allow(clippy::derive_partial_eq_without_eq)] -pub mod sdk { - tonic::include_proto!("sdk/proto"); -} +pub use services::*; diff --git a/protobufs/tests/wasm_test.rs b/protobufs/tests/wasm_test.rs new file mode 100644 index 00000000..6c35f770 --- /dev/null +++ b/protobufs/tests/wasm_test.rs @@ -0,0 +1,85 @@ +use wasm_bindgen_test::*; +use hedera_proto::proto::{AccountId, Timestamp, TransactionBody, TransferList, AccountAmount, TransactionId, CryptoTransferTransactionBody, Duration}; + +wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn test_protobuf_types_in_wasm() { + let account = AccountId { + shard_num: 0, + realm_num: 0, + account: Some(hedera_proto::proto::account_id::Account::AccountNum(3)), + }; + + let timestamp = Timestamp { + seconds: 1640995200, + nanos: 0, + }; + + assert_eq!(account.shard_num, 0); + assert_eq!(timestamp.seconds, 1640995200); +} + +#[wasm_bindgen_test] +fn test_transaction_body_in_wasm() { + let sender = AccountId { + shard_num: 0, + realm_num: 0, + account: Some(hedera_proto::proto::account_id::Account::AccountNum(1001)), + }; + + let receiver = AccountId { + shard_num: 0, + realm_num: 0, + account: Some(hedera_proto::proto::account_id::Account::AccountNum(1002)), + }; + + let transaction_id = TransactionId { + transaction_valid_start: Some(Timestamp { + seconds: 1640995200, + nanos: 0, + }), + account_id: Some(sender.clone()), + scheduled: false, + nonce: 0, + }; + + let transfer = AccountAmount { + account_id: Some(receiver.clone()), + amount: 1000, + is_approval: false, + }; + + let transfers = TransferList { + account_amounts: vec![transfer], + }; + + let crypto_transfer = CryptoTransferTransactionBody { + transfers: Some(transfers), + token_transfers: Vec::new(), + }; + + let transaction_body = TransactionBody { + transaction_id: Some(transaction_id), + node_account_id: Some(AccountId { + shard_num: 0, + realm_num: 0, + account: Some(hedera_proto::proto::account_id::Account::AccountNum(3)), + }), + transaction_fee: 100000, + transaction_valid_duration: Some(Duration { + seconds: 120, + }), + generate_record: false, + memo: String::new(), + data: Some(hedera_proto::proto::transaction_body::Data::CryptoTransfer(crypto_transfer)), + }; + + assert_eq!(transaction_body.transaction_fee, 100000); + assert!(matches!(transaction_body.data, Some(hedera_proto::proto::transaction_body::Data::CryptoTransfer(_)))); + if let Some(hedera_proto::proto::transaction_body::Data::CryptoTransfer(transfer_body)) = transaction_body.data { + let transfers = transfer_body.transfers.unwrap(); + assert_eq!(transfers.account_amounts.len(), 1); + assert_eq!(transfers.account_amounts[0].amount, 1000); + } +}