Skip to content

Commit

Permalink
rest: introduce transaction resolve endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
bmwill committed Oct 2, 2024
1 parent 9f1fae7 commit ce5ead5
Show file tree
Hide file tree
Showing 10 changed files with 966 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/sui-e2e-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ sui-sdk.workspace = true
sui-keys.workspace = true
sui-rest-api.workspace = true
shared-crypto.workspace = true
sui-sdk2.workspace = true

passkey-types.workspace = true
passkey-client.workspace = true
Expand Down
311 changes: 311 additions & 0 deletions crates/sui-e2e-tests/tests/rest.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use shared_crypto::intent::Intent;
use sui_keys::keystore::AccountKeystore;
use sui_macros::sim_test;
use sui_rest_api::client::reqwest::StatusCode;
use sui_rest_api::client::BalanceChange;
use sui_rest_api::Client;
use sui_rest_api::ExecuteTransactionQueryParameters;
use sui_sdk2::types::Argument;
use sui_sdk2::types::Command;
use sui_sdk2::types::TransactionExpiration;
use sui_sdk2::types::UnresolvedGasPayment;
use sui_sdk2::types::UnresolvedInputArgument;
use sui_sdk2::types::UnresolvedObjectReference;
use sui_sdk2::types::UnresolvedProgrammableTransaction;
use sui_sdk2::types::UnresolvedTransaction;
use sui_test_transaction_builder::make_transfer_sui_transaction;
use sui_types::base_types::SuiAddress;
use sui_types::effects::TransactionEffectsAPI;
Expand Down Expand Up @@ -53,3 +64,303 @@ async fn execute_transaction_transfer() {

assert_eq!(actual, expected);
}

#[sim_test]
async fn resolve_transaction_simple_transfer() {
let test_cluster = TestClusterBuilder::new().build().await;

let client = Client::new(test_cluster.rpc_url());
let recipient = SuiAddress::random_for_testing_only();

let (sender, mut gas) = test_cluster.wallet.get_one_account().await.unwrap();
gas.sort_by_key(|object_ref| object_ref.0);
let obj_to_send = gas.first().unwrap().0;

let unresolved_transaction = UnresolvedTransaction {
ptb: UnresolvedProgrammableTransaction {
inputs: vec![
UnresolvedInputArgument::ImmutableOrOwned(UnresolvedObjectReference {
object_id: obj_to_send.into(),
version: None,
digest: None,
}),
UnresolvedInputArgument::Pure {
value: bcs::to_bytes(&recipient).unwrap(),
},
],
commands: vec![Command::TransferObjects(sui_sdk2::types::TransferObjects {
objects: vec![Argument::Input(0)],
address: Argument::Input(1),
})],
},
sender: sender.into(),
gas_payment: None,
expiration: TransactionExpiration::None,
};

let resolved = client
.inner()
.resolve_transaction(&unresolved_transaction)
.await
.unwrap()
.into_inner();

let signed_transaction = test_cluster
.wallet
.sign_transaction(&resolved.transaction.into());
let effects = client
.execute_transaction(
&ExecuteTransactionQueryParameters::default(),
&signed_transaction,
)
.await
.unwrap()
.effects;

assert!(effects.status().is_ok());
assert_eq!(resolved.effects, effects.into());
}

#[sim_test]
async fn resolve_transaction_transfer_with_sponsor() {
let test_cluster = TestClusterBuilder::new().build().await;

let client = Client::new(test_cluster.rpc_url());
let recipient = SuiAddress::random_for_testing_only();

let (sender, gas) = test_cluster.wallet.get_one_account().await.unwrap();
let obj_to_send = gas.first().unwrap().0;
let sponsor = test_cluster.wallet.get_addresses()[1];

let unresolved_transaction = UnresolvedTransaction {
ptb: UnresolvedProgrammableTransaction {
inputs: vec![
UnresolvedInputArgument::ImmutableOrOwned(UnresolvedObjectReference {
object_id: obj_to_send.into(),
version: None,
digest: None,
}),
UnresolvedInputArgument::Pure {
value: bcs::to_bytes(&recipient).unwrap(),
},
],
commands: vec![Command::TransferObjects(sui_sdk2::types::TransferObjects {
objects: vec![Argument::Input(0)],
address: Argument::Input(1),
})],
},
sender: sender.into(),
gas_payment: Some(UnresolvedGasPayment {
objects: vec![],
owner: sponsor.into(),
price: None,
budget: None,
}),
expiration: TransactionExpiration::None,
};

let resolved = client
.inner()
.resolve_transaction(&unresolved_transaction)
.await
.unwrap()
.into_inner();

let transaction_data = resolved.transaction.clone().into();
let sender_sig = test_cluster
.wallet
.config
.keystore
.sign_secure(&sender, &transaction_data, Intent::sui_transaction())
.unwrap();
let sponsor_sig = test_cluster
.wallet
.config
.keystore
.sign_secure(&sponsor, &transaction_data, Intent::sui_transaction())
.unwrap();

let signed_transaction = sui_types::transaction::Transaction::from_data(
transaction_data,
vec![sender_sig, sponsor_sig],
);
let effects = client
.execute_transaction(
&ExecuteTransactionQueryParameters::default(),
&signed_transaction,
)
.await
.unwrap()
.effects;

assert!(effects.status().is_ok());
assert_eq!(resolved.effects, effects.into());
}

#[sim_test]
async fn resolve_transaction_borrowed_shared_object() {
let test_cluster = TestClusterBuilder::new().build().await;

let client = Client::new(test_cluster.rpc_url());

let sender = test_cluster.wallet.get_addresses()[0];

let unresolved_transaction = UnresolvedTransaction {
ptb: UnresolvedProgrammableTransaction {
inputs: vec![UnresolvedInputArgument::Shared {
object_id: "0x6".parse().unwrap(),
initial_shared_version: None,
mutable: None,
}],
commands: vec![Command::MoveCall(sui_sdk2::types::MoveCall {
package: "0x2".parse().unwrap(),
module: "clock".parse().unwrap(),
function: "timestamp_ms".parse().unwrap(),
type_arguments: vec![],
arguments: vec![Argument::Input(0)],
})],
},
sender: sender.into(),
gas_payment: None,
expiration: TransactionExpiration::None,
};

let resolved = client
.inner()
.resolve_transaction(&unresolved_transaction)
.await
.unwrap()
.into_inner();

let signed_transaction = test_cluster
.wallet
.sign_transaction(&resolved.transaction.into());
let effects = client
.execute_transaction(
&ExecuteTransactionQueryParameters::default(),
&signed_transaction,
)
.await
.unwrap()
.effects;

assert!(effects.status().is_ok());
}

#[sim_test]
async fn resolve_transaction_mutable_shared_object() {
let test_cluster = TestClusterBuilder::new().build().await;

let client = Client::new(test_cluster.rpc_url());

let (sender, mut gas) = test_cluster.wallet.get_one_account().await.unwrap();
gas.sort_by_key(|object_ref| object_ref.0);
let obj_to_stake = gas.first().unwrap().0;
let validator_address = client
.inner()
.get_system_state_summary()
.await
.unwrap()
.inner()
.active_validators
.first()
.unwrap()
.address;

let unresolved_transaction = UnresolvedTransaction {
ptb: UnresolvedProgrammableTransaction {
inputs: vec![
UnresolvedInputArgument::Shared {
object_id: "0x5".parse().unwrap(),
initial_shared_version: None,
mutable: None,
},
UnresolvedInputArgument::ImmutableOrOwned(UnresolvedObjectReference {
object_id: obj_to_stake.into(),
version: None,
digest: None,
}),
UnresolvedInputArgument::Pure {
value: bcs::to_bytes(&validator_address).unwrap(),
},
],
commands: vec![Command::MoveCall(sui_sdk2::types::MoveCall {
package: "0x3".parse().unwrap(),
module: "sui_system".parse().unwrap(),
function: "request_add_stake".parse().unwrap(),
type_arguments: vec![],
arguments: vec![Argument::Input(0), Argument::Input(1), Argument::Input(2)],
})],
},
sender: sender.into(),
gas_payment: None,
expiration: TransactionExpiration::None,
};

let resolved = client
.inner()
.resolve_transaction(&unresolved_transaction)
.await
.unwrap()
.into_inner();

let signed_transaction = test_cluster
.wallet
.sign_transaction(&resolved.transaction.into());
let effects = client
.execute_transaction(
&ExecuteTransactionQueryParameters::default(),
&signed_transaction,
)
.await
.unwrap()
.effects;

assert!(effects.status().is_ok());
assert_eq!(resolved.effects, effects.into());
}

#[sim_test]
async fn resolve_transaction_insufficient_gas() {
let test_cluster = TestClusterBuilder::new().build().await;
let client = Client::new(test_cluster.rpc_url());

// Test the case where we don't have enough coins/gas for the required budget
let unresolved_transaction = UnresolvedTransaction {
ptb: UnresolvedProgrammableTransaction {
inputs: vec![UnresolvedInputArgument::Shared {
object_id: "0x6".parse().unwrap(),
initial_shared_version: None,
mutable: None,
}],
commands: vec![Command::MoveCall(sui_sdk2::types::MoveCall {
package: "0x2".parse().unwrap(),
module: "clock".parse().unwrap(),
function: "timestamp_ms".parse().unwrap(),
type_arguments: vec![],
arguments: vec![Argument::Input(0)],
})],
},
sender: SuiAddress::random_for_testing_only().into(), // random account with no gas
gas_payment: None,
expiration: TransactionExpiration::None,
};

let error = client
.inner()
.resolve_transaction(&unresolved_transaction)
.await
.unwrap_err();

assert_eq!(error.status(), Some(StatusCode::BAD_REQUEST));
assert_contains(
error.message().unwrap_or_default(),
"unable to select sufficient gas",
);
}

fn assert_contains(haystack: &str, needle: &str) {
if !haystack.contains(needle) {
panic!("'{haystack}' does not contain '{needle}'");
}
}
1 change: 1 addition & 0 deletions crates/sui-rest-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ fastcrypto.workspace = true
sui-types.workspace = true
mysten-network.workspace = true
sui-protocol-config.workspace = true
move-binary-format.workspace = true

[dev-dependencies]
diffy = "0.3"
Loading

0 comments on commit ce5ead5

Please sign in to comment.