Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions cmd/crates/soroban-test/tests/it/integration/hello_world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,43 @@ async fn invoke_view_with_non_existent_source_account() {
assert_eq!(res, TxnResult::Res(format!(r#"["Hello",{world:?}]"#)));
}

#[tokio::test]
async fn invoke_with_muxed_source_account() {
let sandbox = &TestEnv::new();
let id = &deploy_hello(sandbox).await;

// A muxed (M…) source is the funded `test` identity's G… account plus a
// mux id (#2645). Sending used to fail at the sequence-number lookup,
// which handed the M… strkey to a G-only parser; `--send=yes` forces the
// send path even though `hello` is read-only. An M-literal carries no
// secret, so signing goes through the identity via `--sign-with-key`.
let g_addr = crate::integration::util::test_address(sandbox);
let ed25519 = stellar_strkey::ed25519::PublicKey::from_string(&g_addr)
.unwrap()
.0;
let muxed = format!(
"{}",
stellar_strkey::ed25519::MuxedAccount { ed25519, id: 7 }
);

sandbox
.new_assert_cmd("contract")
.arg("invoke")
.arg("--id")
.arg(id)
.arg("--source-account")
.arg(&muxed)
.arg("--sign-with-key")
.arg("test")
.arg("--send=yes")
.arg("--")
.arg("hello")
.arg("--world=muxed")
.assert()
.success()
.stdout(predicates::str::contains(r#"["Hello","muxed"]"#));
}

#[tokio::test]
#[allow(clippy::too_many_lines)]
async fn invoke_contract() {
Expand Down
39 changes: 29 additions & 10 deletions cmd/crates/soroban-test/tests/it/integration/token/transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,26 +219,45 @@ async fn transfer_rejects_negative_amount_before_any_rpc() {
}

#[tokio::test]
async fn transfer_rejects_muxed_source_with_clear_error() {
async fn transfer_from_muxed_source() {
let sandbox = &TestEnv::new();
let recipient = new_account(sandbox, "recipient");

deploy_sac(sandbox, "native", "test");
let sac = sac_id(sandbox, "native");

// A muxed (M…) source is the funded `test` identity's G… account plus a
// mux id (#2645). An M-literal carries no secret, so signing goes through
// the identity via `--sign-with-key`.
let g_addr = test_address(sandbox);
let ed25519 = stellar_strkey::ed25519::PublicKey::from_string(&g_addr)
.unwrap()
.0;
let muxed = format!(
"{}",
stellar_strkey::ed25519::MuxedAccount { ed25519, id: 1 }
);

// Muxed (M…) source accounts aren't supported by the invoke pipeline yet
// (see #2645). Until then the command must reject them up front with a clear
// message rather than a raw strkey decode error deep in the pipeline.
let muxed = "MA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAAAAAAAAAPCICBKU";
let recipient_before = sac_balance(sandbox, &sac, &recipient);
sandbox
.new_assert_cmd("token")
.args([
"transfer", "--id", "native", "--to", &recipient, "--amount", "1", "--from", muxed,
"transfer",
"--id",
"native",
"--to",
&recipient,
"--amount",
"1",
"--from",
&muxed,
"--sign-with-key",
"test",
])
.assert()
.failure()
.stderr(predicates::str::contains(
"muxed (M…) source accounts are not yet supported",
));
.success();

assert_eq!(sac_balance(sandbox, &sac, &recipient), recipient_before + 1);
}

#[tokio::test]
Expand Down
5 changes: 4 additions & 1 deletion cmd/soroban-cli/src/commands/contract/invoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,11 @@ impl Cmd {
.verify_network_passphrase(Some(&network.network_passphrase))
.await?;

// A muxed (M…) source is an underlying G… account plus a mux id;
// only the underlying account exists on ledger, so the sequence
// number lookup must use its G… form (#2645).
client
.get_account(&config.source_account()?.to_string())
.get_account(&config.source_account()?.account_id().to_string())
.await?
} else {
if should_send == ShouldSend::DefaultNo {
Expand Down
25 changes: 7 additions & 18 deletions cmd/soroban-cli/src/commands/token/transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,6 @@ pub enum Error {
Invoke(#[from] invoke::Error),
#[error(transparent)]
Serde(#[from] serde_json::Error),

#[error(
"muxed (M…) source accounts are not yet supported for `token transfer`; \
use the underlying G… account as `--from` instead"
)]
MuxedSourceNotSupported,
}

/// Parse `--amount` as a non-negative `i128`. A negative transfer amount is
Expand Down Expand Up @@ -101,7 +95,6 @@ impl Error {
Error::ScAddress(_) => "invalid_address",
Error::Invoke(_) => "invoke",
Error::Serde(_) => "internal",
Error::MuxedSourceNotSupported => "unsupported",
}
}
}
Expand Down Expand Up @@ -146,17 +139,13 @@ impl Cmd {
.resolve(&config.locator, &network.network_passphrase)?;

// SEP-41 `transfer(from, to, amount)`: `from` is the source account
// (which also signs and authorizes), `to` is the destination.
//
// The invoke pipeline can't source a transaction from a muxed account
// yet (see #2645), and a muxed strkey in the `transfer` arg is rejected
// mid-simulation with an opaque host error; reject it up front with a
// clear message instead.
let source_account = config.source_account()?;
if matches!(source_account, crate::xdr::MuxedAccount::MuxedEd25519(_)) {
return Err(Error::MuxedSourceNotSupported);
}
let from = source_account.to_string();
// (which also signs and authorizes), `to` is the destination. A muxed
// (M…) `from` resolves to its underlying G… account: `from` must
// authorize the transfer, muxed accounts are not valid authorizers,
// and the host rejects a muxed `from` in simulation with an opaque
// `Error(Value, UnexpectedType)` (#2645). A muxed `--to` flows through
// as-is, preserving the mux id in the `transfer` argument and events.
let from = config.source_account()?.account_id().to_string();
// `--to` may be an account (`G…`/`M…`), a contract (`C…`), or an alias;
// resolve it to an `ScAddress` and hand the strkey to the `transfer`
// arg, which accepts any of these destinations.
Expand Down