diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml index d913cf23d..45d74a243 100644 --- a/.github/workflows/binaries.yml +++ b/.github/workflows/binaries.yml @@ -157,6 +157,6 @@ jobs: repo: context.repo.repo, release_id: ${{ github.event.release.id }}, name: '${{ env.STELLAR_CLI_INSTALLER }}', - data: fs.readFileSync('Output/stellar-installer.exe'), + data: fs.readFileSync('${{ env.STELLAR_CLI_INSTALLER }}'), }); diff --git a/.github/workflows/bindings-ts.yml b/.github/workflows/bindings-ts.yml index 9b5f767ac..f6aaae499 100644 --- a/.github/workflows/bindings-ts.yml +++ b/.github/workflows/bindings-ts.yml @@ -19,7 +19,7 @@ jobs: NETWORK: local ENABLE_SOROBAN_RPC: true options: >- - --health-cmd "curl --no-progress-meter --fail-with-body -X POST \"http://localhost:8000/soroban/rpc\" -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":8675309,\"method\":\"getNetwork\"}' && curl --no-progress-meter \"http://localhost:8000/friendbot\" | grep '\"invalid_field\": \"addr\"'" + --health-cmd "curl --no-progress-meter --fail-with-body -X POST \"http://localhost:8000/rpc\" -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":8675309,\"method\":\"getNetwork\"}' && curl --no-progress-meter \"http://localhost:8000/friendbot\" | grep '\"invalid_field\": \"addr\"'" --health-interval 10s --health-timeout 5s --health-retries 50 diff --git a/cmd/crates/soroban-spec-typescript/ts-tests/.env b/cmd/crates/soroban-spec-typescript/ts-tests/.env index 93bb4be67..0ea150c3f 100644 --- a/cmd/crates/soroban-spec-typescript/ts-tests/.env +++ b/cmd/crates/soroban-spec-typescript/ts-tests/.env @@ -1,3 +1,3 @@ -SOROBAN_NETWORK_PASSPHRASE="Standalone Network ; February 2017" -SOROBAN_RPC_URL="http://localhost:8000/soroban/rpc" -SOROBAN_FRIENDBOT_URL="http://localhost:8000/friendbot" +STELLAR_NETWORK_PASSPHRASE="Standalone Network ; February 2017" +STELLAR_RPC_URL="http://localhost:8000/rpc" +STELLAR_FRIENDBOT_URL="http://localhost:8000/friendbot" diff --git a/cmd/crates/soroban-spec-typescript/ts-tests/initialize.sh b/cmd/crates/soroban-spec-typescript/ts-tests/initialize.sh index 804820abb..1fb84f081 100755 --- a/cmd/crates/soroban-spec-typescript/ts-tests/initialize.sh +++ b/cmd/crates/soroban-spec-typescript/ts-tests/initialize.sh @@ -10,10 +10,10 @@ done unset IFS echo Network -echo " RPC: $SOROBAN_RPC_URL" -echo " Passphrase: \"$SOROBAN_NETWORK_PASSPHRASE\"" +echo " RPC: $STELLAR_RPC_URL" +echo " Passphrase: \"$STELLAR_NETWORK_PASSPHRASE\"" -NETWORK_STATUS=$(curl -s -X POST "http://localhost:8000/soroban/rpc" -H "Content-Type: application/json" -d '{ "jsonrpc": "2.0", "id": 8675309, "method": "getHealth" }' | sed 's/.*"status":"\([^"]*\)".*/\1/') || { echo "Make sure you're running local RPC network on localhost:8000" && exit 1; } +NETWORK_STATUS=$(curl -s -X POST "http://localhost:8000/rpc" -H "Content-Type: application/json" -d '{ "jsonrpc": "2.0", "id": 8675309, "method": "getHealth" }' | sed 's/.*"status":"\([^"]*\)".*/\1/') || { echo "Make sure you're running local RPC network on localhost:8000" && exit 1; } echo " Status: $NETWORK_STATUS" if [[ "$NETWORK_STATUS" != "healthy" ]]; then @@ -22,28 +22,28 @@ if [[ "$NETWORK_STATUS" != "healthy" ]]; then fi # Print command before executing, from https://stackoverflow.com/a/23342259/249801 -# Discussion: https://github.com/stellar/soroban-tools/pull/1034#pullrequestreview-1690667116 +# Discussion: https://github.com/stellar/stellar-tools/pull/1034#pullrequestreview-1690667116 exe() { echo"${@/eval/}" ; "$@" ; } function fund_all() { - exe eval "./soroban keys generate root" - exe eval "./soroban keys fund root" + exe eval "./stellar keys generate --fund root" } function upload() { - exe eval "(./soroban contract $1 --quiet --source root --wasm $2 --ignore-checks) > $3" + exe eval "(./stellar contract $1 --quiet --source root --wasm $2 --ignore-checks) > $3" } function deploy_all() { upload deploy ../../../../target/wasm32-unknown-unknown/test-wasms/test_custom_types.wasm contract-id-custom-types.txt - # TODO: support `--wasm-hash` with `contract bindings` upload install ../../../../target/wasm32-unknown-unknown/test-wasms/test_constructor.wasm contract-wasm-hash-constructor.txt + exe eval "./stellar contract asset deploy --asset native --source root" } function bind() { - exe eval "./soroban contract bindings typescript $1 $2 --output-dir ./node_modules/$3 --overwrite" + exe eval "./stellar contract bindings typescript $1 $2 --output-dir ./node_modules/$3 --overwrite" exe eval "sh -c \"cd ./node_modules/$3 && npm install && npm run build\"" } function bind_all() { bind --contract-id $(cat contract-id-custom-types.txt) test-custom-types bind --wasm-hash $(cat contract-wasm-hash-constructor.txt) test-constructor + bind --contract-id $(./stellar contract id asset --asset native) xlm } fund_all diff --git a/cmd/crates/soroban-spec-typescript/ts-tests/src/test-xlm-lib-from-sac.ts b/cmd/crates/soroban-spec-typescript/ts-tests/src/test-xlm-lib-from-sac.ts new file mode 100644 index 000000000..e39fb8b7d --- /dev/null +++ b/cmd/crates/soroban-spec-typescript/ts-tests/src/test-xlm-lib-from-sac.ts @@ -0,0 +1,15 @@ +import test from "ava" +import { rpcUrl, root, signer } from "./util.js" +import { Client, networks } from "xlm" + +const contract = new Client({ + ...networks.standalone, + rpcUrl, + allowHttp: true, + publicKey: root.keypair.publicKey(), + ...signer, +}) + +test("can generate a lib from a Stellar Asset Contract", async (t) => { + t.is((await contract.symbol()).result, "native"); +}); diff --git a/cmd/crates/soroban-spec-typescript/ts-tests/src/util.ts b/cmd/crates/soroban-spec-typescript/ts-tests/src/util.ts index a5315a643..15649b63e 100644 --- a/cmd/crates/soroban-spec-typescript/ts-tests/src/util.ts +++ b/cmd/crates/soroban-spec-typescript/ts-tests/src/util.ts @@ -3,7 +3,7 @@ import { Address, Keypair } from "@stellar/stellar-sdk"; import { basicNodeSigner } from "@stellar/stellar-sdk/contract"; const rootKeypair = Keypair.fromSecret( - spawnSync("./soroban", ["keys", "show", "root"], { + spawnSync("./stellar", ["keys", "show", "root"], { shell: true, encoding: "utf8", }).stdout.trim(), @@ -14,9 +14,9 @@ export const root = { address: Address.fromString(rootKeypair.publicKey()), }; -export const rpcUrl = process.env.SOROBAN_RPC_URL ?? "http://localhost:8000/"; +export const rpcUrl = process.env.STELLAR_RPC_URL ?? "http://localhost:8000/"; export const networkPassphrase = - process.env.SOROBAN_NETWORK_PASSPHRASE ?? + process.env.STELLAR_NETWORK_PASSPHRASE ?? "Standalone Network ; February 2017"; export const signer = basicNodeSigner(root.keypair, networkPassphrase); diff --git a/cmd/crates/soroban-spec-typescript/ts-tests/soroban b/cmd/crates/soroban-spec-typescript/ts-tests/stellar similarity index 100% rename from cmd/crates/soroban-spec-typescript/ts-tests/soroban rename to cmd/crates/soroban-spec-typescript/ts-tests/stellar diff --git a/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs b/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs index abe26dd37..1da64eaa5 100644 --- a/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs +++ b/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs @@ -1,12 +1,12 @@ use std::{ffi::OsString, fmt::Debug, path::PathBuf}; use clap::{command, Parser}; -use soroban_spec_tools::contract as contract_spec; +use soroban_spec_tools::contract as spec_tools; use soroban_spec_typescript::boilerplate::Project; use crate::print::Print; use crate::{ - commands::{contract::info::shared as wasm_or_contract, global, NetworkRunnable}, + commands::{contract::info::shared as contract_spec, global, NetworkRunnable}, config, }; use soroban_spec_tools::contract::Spec; @@ -15,7 +15,7 @@ use soroban_spec_tools::contract::Spec; #[group(skip)] pub struct Cmd { #[command(flatten)] - pub wasm_or_hash_or_contract_id: wasm_or_contract::Args, + pub wasm_or_hash_or_contract_id: contract_spec::Args, /// Where to place generated project #[arg(long)] pub output_dir: PathBuf, @@ -39,11 +39,13 @@ pub enum Error { NotUtf8(OsString), #[error(transparent)] - Spec(#[from] contract_spec::Error), + Spec(#[from] spec_tools::Error), #[error("Failed to get file name from path: {0:?}")] FailedToGetFileName(PathBuf), #[error(transparent)] - WasmOrContract(#[from] wasm_or_contract::Error), + WasmOrContract(#[from] contract_spec::Error), + #[error(transparent)] + Xdr(#[from] crate::xdr::Error), } #[async_trait::async_trait] @@ -58,13 +60,14 @@ impl NetworkRunnable for Cmd { ) -> Result<(), Error> { let print = Print::new(global_args.is_some_and(|a| a.quiet)); - let (spec, contract_address, network) = - wasm_or_contract::fetch_wasm(&self.wasm_or_hash_or_contract_id, &print).await?; + let contract_spec::Fetched { contract, source } = + contract_spec::fetch(&self.wasm_or_hash_or_contract_id, &print).await?; - let spec = if let Some(spec) = spec { - Spec::new(&spec)? - } else { - Spec::new(&soroban_sdk::token::StellarAssetSpec::spec_xdr())? + let spec = match contract { + contract_spec::Contract::Wasm { wasm_bytes } => Spec::new(&wasm_bytes)?.spec, + contract_spec::Contract::StellarAssetContract => { + soroban_spec::read::parse_raw(&soroban_sdk::token::StellarAssetSpec::spec_xdr())? + } }; if self.output_dir.is_file() { @@ -86,15 +89,23 @@ impl NetworkRunnable for Cmd { let contract_name = &file_name .to_str() .ok_or_else(|| Error::NotUtf8(file_name.to_os_string()))?; - if let Some(contract_address) = contract_address.clone() { - print.infoln(format!("Embedding contract address: {contract_address}")); - } + let (resolved_address, network) = match source { + contract_spec::Source::Contract { + resolved_address, + network, + } => { + print.infoln(format!("Embedding contract address: {resolved_address}")); + (Some(resolved_address), Some(network)) + } + contract_spec::Source::Wasm { network, .. } => (None, Some(network)), + contract_spec::Source::File { .. } => (None, None), + }; p.init( contract_name, - contract_address.as_deref(), + resolved_address.as_deref(), network.as_ref().map(|n| n.rpc_url.as_ref()), network.as_ref().map(|n| n.network_passphrase.as_ref()), - &spec.spec, + &spec, )?; print.checkln("Generated!"); print.infoln(format!( diff --git a/cmd/soroban-cli/src/commands/contract/info/env_meta.rs b/cmd/soroban-cli/src/commands/contract/info/env_meta.rs index f882d2737..02da6f439 100644 --- a/cmd/soroban-cli/src/commands/contract/info/env_meta.rs +++ b/cmd/soroban-cli/src/commands/contract/info/env_meta.rs @@ -9,7 +9,7 @@ use crate::{ commands::{ contract::info::{ env_meta::Error::{NoEnvMetaPresent, NoSACEnvMeta}, - shared::{self, fetch_wasm, MetasInfoOutput}, + shared::{self, fetch, Fetched, MetasInfoOutput}, }, global, }, @@ -43,12 +43,12 @@ pub enum Error { impl Cmd { pub async fn run(&self, global_args: &global::Args) -> Result { let print = Print::new(global_args.quiet); - let (bytes, ..) = fetch_wasm(&self.common, &print).await?; + let Fetched { contract, .. } = fetch(&self.common, &print).await?; - let Some(bytes) = bytes else { - return Err(NoSACEnvMeta()); + let spec = match contract { + shared::Contract::Wasm { wasm_bytes } => Spec::new(&wasm_bytes)?, + shared::Contract::StellarAssetContract => return Err(NoSACEnvMeta()), }; - let spec = Spec::new(&bytes)?; let Some(env_meta_base64) = spec.env_meta_base64 else { return Err(NoEnvMetaPresent()); diff --git a/cmd/soroban-cli/src/commands/contract/info/interface.rs b/cmd/soroban-cli/src/commands/contract/info/interface.rs index dd71150fa..0c896bc3d 100644 --- a/cmd/soroban-cli/src/commands/contract/info/interface.rs +++ b/cmd/soroban-cli/src/commands/contract/info/interface.rs @@ -1,8 +1,7 @@ use std::fmt::Debug; use crate::commands::contract::info::interface::Error::NoInterfacePresent; -use crate::commands::contract::info::shared; -use crate::commands::contract::info::shared::fetch_wasm; +use crate::commands::contract::info::shared::{self, fetch, Fetched}; use crate::commands::global; use crate::print::Print; use clap::{command, Parser}; @@ -47,18 +46,21 @@ pub enum Error { impl Cmd { pub async fn run(&self, global_args: &global::Args) -> Result { let print = Print::new(global_args.quiet); - let (bytes, ..) = fetch_wasm(&self.common, &print).await?; + let Fetched { contract, .. } = fetch(&self.common, &print).await?; - let (base64, spec) = if bytes.is_none() { - Spec::spec_to_base64(&soroban_sdk::token::StellarAssetSpec::spec_xdr())? - } else { - let spec = Spec::new(&bytes.unwrap())?; + let (base64, spec) = match contract { + shared::Contract::Wasm { wasm_bytes } => { + let spec = Spec::new(&wasm_bytes)?; - if spec.env_meta_base64.is_none() { - return Err(NoInterfacePresent()); - } + if spec.env_meta_base64.is_none() { + return Err(NoInterfacePresent()); + } - (spec.spec_base64.unwrap(), spec.spec) + (spec.spec_base64.unwrap(), spec.spec) + } + shared::Contract::StellarAssetContract => { + Spec::spec_to_base64(&soroban_sdk::token::StellarAssetSpec::spec_xdr())? + } }; let res = match self.output { diff --git a/cmd/soroban-cli/src/commands/contract/info/meta.rs b/cmd/soroban-cli/src/commands/contract/info/meta.rs index 4b6fdee3e..736e8f432 100644 --- a/cmd/soroban-cli/src/commands/contract/info/meta.rs +++ b/cmd/soroban-cli/src/commands/contract/info/meta.rs @@ -1,8 +1,7 @@ use std::fmt::Debug; use crate::commands::contract::info::meta::Error::{NoMetaPresent, NoSACMeta}; -use crate::commands::contract::info::shared; -use crate::commands::contract::info::shared::{fetch_wasm, MetasInfoOutput}; +use crate::commands::contract::info::shared::{self, fetch, Fetched, MetasInfoOutput}; use crate::commands::global; use crate::print::Print; use clap::{command, Parser}; @@ -36,12 +35,12 @@ pub enum Error { impl Cmd { pub async fn run(&self, global_args: &global::Args) -> Result { let print = Print::new(global_args.quiet); - let (bytes, ..) = fetch_wasm(&self.common, &print).await?; + let Fetched { contract, .. } = fetch(&self.common, &print).await?; - let Some(bytes) = bytes else { - return Err(NoSACMeta()); + let spec = match contract { + shared::Contract::Wasm { wasm_bytes } => Spec::new(&wasm_bytes)?, + shared::Contract::StellarAssetContract => return Err(NoSACMeta()), }; - let spec = Spec::new(&bytes)?; let Some(meta_base64) = spec.meta_base64 else { return Err(NoMetaPresent()); diff --git a/cmd/soroban-cli/src/commands/contract/info/shared.rs b/cmd/soroban-cli/src/commands/contract/info/shared.rs index 2ee5d018d..6023b03cf 100644 --- a/cmd/soroban-cli/src/commands/contract/info/shared.rs +++ b/cmd/soroban-cli/src/commands/contract/info/shared.rs @@ -76,23 +76,58 @@ pub enum Error { #[error("provided wasm hash is invalid {0:?}")] InvalidWasmHash(String), #[error("must provide one of --wasm, --wasm-hash, or --contract-id")] - MalformedWasmOrWasmHashOrContractId, + MissingArg, #[error(transparent)] Rpc(#[from] soroban_rpc::Error), #[error(transparent)] Locator(#[from] locator::Error), } -pub async fn fetch_wasm( - args: &Args, - print: &Print, -) -> Result<(Option>, Option, Option), Error> { +pub struct Fetched { + pub contract: Contract, + pub source: Source, +} + +pub enum Contract { + Wasm { wasm_bytes: Vec }, + StellarAssetContract, +} + +pub enum Source { + File { + path: PathBuf, + }, + Wasm { + hash: String, + network: Network, + }, + Contract { + resolved_address: String, + network: Network, + }, +} + +impl Source { + pub fn network(&self) -> Option<&Network> { + match self { + Source::File { .. } => None, + Source::Wasm { ref network, .. } | Source::Contract { ref network, .. } => { + Some(network) + } + } + } +} + +pub async fn fetch(args: &Args, print: &Print) -> Result { // Check if a local WASM file path is provided if let Some(path) = &args.wasm { // Read the WASM file and return its contents print.infoln("Loading contract spec from file..."); let wasm_bytes = wasm::Args { wasm: path.clone() }.read()?; - return Ok((Some(wasm_bytes), None, None)); + return Ok(Fetched { + contract: Contract::Wasm { wasm_bytes }, + source: Source::File { path: path.clone() }, + }); } // If no local wasm, then check for wasm_hash and fetch from the network @@ -116,22 +151,37 @@ pub async fn fetch_wasm( print.globeln(format!( "Downloading contract spec for wasm hash: {wasm_hash}" )); - Ok(( - Some(get_remote_wasm_from_hash(&client, &hash).await?), - None, - Some(network.clone()), - )) + let wasm_bytes = get_remote_wasm_from_hash(&client, &hash).await?; + Ok(Fetched { + contract: Contract::Wasm { wasm_bytes }, + source: Source::Wasm { + hash: wasm_hash.clone(), + network: network.clone(), + }, + }) } else if let Some(contract_id) = &args.contract_id { let contract_id = contract_id.resolve_contract_id(&args.locator, &network.network_passphrase)?; - let contract_address = xdr::ScAddress::Contract(xdr::Hash(contract_id.0)).to_string(); - print.globeln(format!("Downloading contract spec: {contract_address}")); + let derived_address = xdr::ScAddress::Contract(xdr::Hash(contract_id.0)).to_string(); + print.globeln(format!("Downloading contract spec: {derived_address}")); let res = wasm::fetch_from_contract(&contract_id, network).await; if let Some(ContractIsStellarAsset) = res.as_ref().err() { - return Ok((None, Some(contract_address), Some(network.clone()))); + return Ok(Fetched { + contract: Contract::StellarAssetContract, + source: Source::Contract { + resolved_address: derived_address, + network: network.clone(), + }, + }); } - Ok((Some(res?), Some(contract_address), Some(network.clone()))) + Ok(Fetched { + contract: Contract::Wasm { wasm_bytes: res? }, + source: Source::Contract { + resolved_address: derived_address, + network: network.clone(), + }, + }) } else { - return Err(Error::MalformedWasmOrWasmHashOrContractId); + return Err(Error::MissingArg); } } diff --git a/cmd/soroban-cli/src/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs index a7fd86fda..d6de1b6ea 100644 --- a/cmd/soroban-cli/src/config/secret.rs +++ b/cmd/soroban-cli/src/config/secret.rs @@ -144,7 +144,7 @@ impl Secret { let seed_phrase = if let Some(seed) = seed.map(str::as_bytes) { sep5::SeedPhrase::from_entropy(seed) } else { - sep5::SeedPhrase::random(sep5::MnemonicType::Words12) + sep5::SeedPhrase::random(sep5::MnemonicType::Words24) }? .seed_phrase .into_phrase();