Signing and Submitting Transactions
In Move-based blockchains, transactions are the primary means to interact with the network, such as invoking smart contract functions or transferring assets.
Understanding Transactions in Move
Transactions in Move represent function calls or operations that affect the state of the blockchain. They are executed based on provided inputs, which can be:
- Object References: These refer to owned objects, immutable objects, or shared objects within the blockchain state.
- Encoded Values: For example, a vector of bytes used as arguments in a Move function call.
After constructing a transaction—typically using Programmable Transaction Blocks (PTBs)—you need to sign it and submit it to the network.
The signature must be generated using your private key, and the corresponding public key must match the sender's blockchain address.
IOTA uses a IotaKeyPair
for signature generation, committing to the Blake2b hash of the intent message (intent || serialized transaction data
). Supported signature schemes include Ed25519
, ECDSA Secp256k1
, ECDSA Secp256r1
, and Multisig
.
You can instantiate key pairs for Ed25519
, ECDSA Secp256k1
, and ECDSA Secp256r1
using IotaKeyPair
and use them to sign transactions.
For Multisig
, please refer to its dedicated guide.
Once you have the signature and the transaction bytes, you can submit the transaction for execution on-chain.
Workflow Overview
Here is a high-level overview of the steps involved in constructing, signing, and executing a transaction:
- Construct the Transaction Data: Create a
Transaction
where you can chain multiple operations. Refer to Creating Programmable Transaction Blocks for guidance. - Select Gas Coin and Estimate Gas: Use the SDK's built-in tools for gas estimation and coin selection.
- Sign the Transaction: Generate a signature using your private key.
- Submit the Transaction: Send the
Transaction
and its signature to the network for execution.
If you wish to specify a particular gas coin, locate the gas coin object ID that you own and include it explicitly in the PTB. If you don't have a gas coin object, use the splitCoin operation to create one. Ensure that the split coin transaction is the first call in the PTB.
Examples
Below are examples demonstrating how to sign and submit transactions using Rust, TypeScript, or the Move CLI.
- TypeScript
- Rust
- IOTA CLI
There are several methods to create a key pair and derive its public key and Move address using the Move TypeScript SDK.
import { fromHEX } from '@iota/bcs';
import { getFullnodeUrl, IOTAClient } from '@iota/iota-sdk/client';
import { type Keypair } from '@iota/iota-sdk/cryptography';
import { Ed25519Keypair } from '@iota/iota-sdk/keypairs/ed25519';
import { Secp256k1Keypair } from '@iota/iota-sdk/keypairs/secp256k1';
import { Secp256r1Keypair } from '@iota/iota-sdk/keypairs/secp256r1';
import { Transaction } from '@iota/iota-sdk/transactions';
const kp_rand_0 = new Ed25519Keypair();
const kp_rand_1 = new Secp256k1Keypair();
const kp_rand_2 = new Secp256r1Keypair();
const kp_import_0 = Ed25519Keypair.fromSecretKey(
fromHex('0xd463e11c7915945e86ac2b72d88b8190cfad8ff7b48e7eb892c275a5cf0a3e82'),
);
const kp_import_1 = Secp256k1Keypair.fromSecretKey(
fromHex('0xd463e11c7915945e86ac2b72d88b8190cfad8ff7b48e7eb892c275a5cf0a3e82'),
);
const kp_import_2 = Secp256r1Keypair.fromSecretKey(
fromHex('0xd463e11c7915945e86ac2b72d88b8190cfad8ff7b48e7eb892c275a5cf0a3e82'),
);
// $MNEMONICS refers to 12/15/18/21/24 words from the wordlist, e.g. "retire skin goose will hurry this field stadium drastic label husband venture cruel toe wire". Refer to [Keys and Addresses](../cryptography/transaction-auth/keys-addresses.mdx) for more.
const kp_derive_0 = Ed25519Keypair.deriveKeypair('$MNEMONICS');
const kp_derive_1 = Secp256k1Keypair.deriveKeypair('$MNEMONICS');
const kp_derive_2 = Secp256r1Keypair.deriveKeypair('$MNEMONICS');
const kp_derive_with_path_0 = Ed25519Keypair.deriveKeypair('$MNEMONICS', "m/44'/4218'/1'/0'/0'");
const kp_derive_with_path_1 = Secp256k1Keypair.deriveKeypair('$MNEMONICS', "m/54'/4218'/1'/0/0");
const kp_derive_with_path_2 = Secp256r1Keypair.deriveKeypair('$MNEMONICS', "m/74'/4218'/1'/0/0");
// replace `kp_rand_0` with the variable names above.
const pk = kp_rand_0.getPublicKey();
const sender = pk.toIOTAAddress();
// create an example transaction block.
const txb = new Transaction();
txb.setSender(sender);
txb.setGasPrice(5);
txb.setGasBudget(100);
const bytes = await txb.build();
const serializedSignature = (await keypair.signTransaction(bytes)).signature;
// verify the signature locally
expect(await keypair.getPublicKey().verifyTransaction(bytes, serializedSignature)).toEqual(
true,
);
// define iota client for the desired network.
const client = new IOTAClient({ url: getFullnodeUrl('testnet') });
// execute transaction.
let res = client.executeTransaction({
Transaction: bytes,
signature: serializedSignature,
});
console.log(res);
The complete code example is available in the iota-sdk.
You can create a IotaKeyPair
and derive its public key and IOTA address using the IOTA Rust SDK.
// deterministically generate a keypair, testing only, do not use for mainnet, use the next section to randomly generate a keypair instead.
let ikp_determ_0 =
IotaKeyPair::Ed25519(Ed25519KeyPair::generate(&mut StdRng::from_seed([0; 32])));
let _ikp_determ_1 =
IotaKeyPair::Secp256k1(Secp256k1KeyPair::generate(&mut StdRng::from_seed([0; 32])));
let _ikp_determ_2 =
IotaKeyPair::Secp256r1(Secp256r1KeyPair::generate(&mut StdRng::from_seed([0; 32])));
// randomly generate a keypair.
let _ikp_rand_0 = IotaKeyPair::Ed25519(get_key_pair_from_rng(&mut rand::rngs::OsRng).1);
let _ikp_rand_1 = IotaKeyPair::Secp256k1(get_key_pair_from_rng(&mut rand::rngs::OsRng).1);
let _ikp_rand_2 = IotaKeyPair::Secp256r1(get_key_pair_from_rng(&mut rand::rngs::OsRng).1);
// import a keypair from a base64 encoded 32-byte `private key`.
let _ikp_import_no_flag_0 = IotaKeyPair::Ed25519(Ed25519KeyPair::from_bytes(
&Base64::decode("1GPhHHkVlF6GrCty2IuBkM+tj/e0jn64ksJ1pc8KPoI=")
.map_err(|_| anyhow!("Invalid base64"))?,
)?);
let _ikp_import_no_flag_1 = IotaKeyPair::Ed25519(Ed25519KeyPair::from_bytes(
&Base64::decode("1GPhHHkVlF6GrCty2IuBkM+tj/e0jn64ksJ1pc8KPoI=")
.map_err(|_| anyhow!("Invalid base64"))?,
)?);
let _ikp_import_no_flag_2 = IotaKeyPair::Ed25519(Ed25519KeyPair::from_bytes(
&Base64::decode("1GPhHHkVlF6GrCty2IuBkM+tj/e0jn64ksJ1pc8KPoI=")
.map_err(|_| anyhow!("Invalid base64"))?,
)?);
// import a keypair from a base64 encoded 33-byte `flag || private key`. The signature scheme is determined by the flag.
let _ikp_import_with_flag_0 =
IotaKeyPair::decode_base64("ANRj4Rx5FZRehqwrctiLgZDPrY/3tI5+uJLCdaXPCj6C")
.map_err(|_| anyhow!("Invalid base64"))?;
let _ikp_import_with_flag_1 =
IotaKeyPair::decode_base64("AdRj4Rx5FZRehqwrctiLgZDPrY/3tI5+uJLCdaXPCj6C")
.map_err(|_| anyhow!("Invalid base64"))?;
let _ikp_import_with_flag_2 =
IotaKeyPair::decode_base64("AtRj4Rx5FZRehqwrctiLgZDPrY/3tI5+uJLCdaXPCj6C")
.map_err(|_| anyhow!("Invalid base64"))?;
// replace `ikp_determ_0` with the variable names above
let pk = ikp_determ_0.public();
let sender = IOTAAddress::from(&pk);
Next, sign transaction data constructed using an example programmable transaction block with default gas coin, gas budget, and gas price. See Building Programmable Transaction Blocks for more information.
// construct an example programmable transaction.
let pt = {
let mut builder = ProgrammableTransactionBuilder::new();
builder.pay_iota(vec![sender], vec![1])?;
builder.finish()
};
let gas_budget = 5_000_000;
let gas_price = iota_client.read_api().get_reference_gas_price().await?;
// create the transaction data that will be sent to the network.
let tx_data = TransactionData::new_programmable(
sender,
vec![gas_coin.object_ref()],
pt,
gas_budget,
gas_price,
);
Commit a signature to the Blake2b hash digest of the intent message (intent || bcs bytes of tx_data
).
// derive the digest that the keypair should sign on, i.e. the blake2b hash of `intent || tx_data`.
let intent_msg = IntentMessage::new(Intent::iota_transaction(), tx_data);
let raw_tx = bcs::to_bytes(&intent_msg).expect("bcs should not fail");
let mut hasher = iota_types::crypto::DefaultHash::default();
hasher.update(raw_tx.clone());
let digest = hasher.finalize().digest;
// use IotaKeyPair to sign the digest.
let iota_sig = ikp_determ_0.sign(&digest);
// if you would like to verify the signature locally before submission, use this function. if it fails to verify locally, the transaction will fail to execute in IOTA.
let res = iota_sig.verify_secure(
&intent_msg,
sender,
iota_types::crypto::SignatureScheme::ED25519,
);
assert!(res.is_ok());
Finally, submit the transaction with the signature.
let transaction_response = iota_client
.quorum_driver_api()
.execute_transaction_block(
iota_types::transaction::Transaction::from_generic_sig_data(
intent_msg.value,
Intent::iota_transaction(),
vec![GenericSignature::Signature(iota_sig)],
),
IOTATransactionResponseOptions::default(),
None,
)
.await?;
When you first use the IOTA CLI, it creates a keystore at ~/.move/keystore
on your machine, storing your private keys. You can sign transactions using any key by specifying its address. Use move keytool list
to view your addresses.
You can initialize a key in three ways:
# generate randomly.
iota client new-address --key-scheme ed25519
iota client new-address --key-scheme secp256k1
iota client new-address --key-scheme secp256r1
# import the 32-byte private key to keystore.
iota keytool import "0xd463e11c7915945e86ac2b72d88b8190cfad8ff7b48e7eb892c275a5cf0a3e82" ed25519
iota keytool import "0xd463e11c7915945e86ac2b72d88b8190cfad8ff7b48e7eb892c275a5cf0a3e82" secp256k1
iota keytool import "0xd463e11c7915945e86ac2b72d88b8190cfad8ff7b48e7eb892c275a5cf0a3e82" secp256r1
# import the mnemonics (recovery phrase) with derivation path to keystore.
# $MNEMONICS refers to 12/15/18/21/24 words from the wordlist, e.g. "retire skin goose will hurry this field stadium drastic label husband venture cruel toe wire". Refer to [Keys and Addresses](../cryptography/transaction-auth/keys-addresses.mdx) for more.
iota keytool import "$MNEMONICS" ed25519
iota keytool import "$MNEMONICS" secp256k1
iota keytool import "$MNEMONICS" secp256r1
Create a transfer transaction in CLI. Set the GAS_COIN_ID refers to the object ID that is owned by the sender to be used as gas. MNEMONICS refers to 12/15/18/21/24 words from the wordlist, e.g. "retire skin goose will hurry this field stadium drastic label husband venture cruel toe wire". Refer to Keys and Addresses for more.
iota client gas
iota client pay-all-iota --input-coins $GAS_COIN_ID --recipient $IOTA_ADDRESS --gas-budget 2000000 --serialize-unsigned-transaction
iota keytool sign --address $IOTA_ADDRESS --data $TX_BYTES
iota client execute-signed-tx --tx-bytes $TX_BYTES --signatures $SERIALIZED_SIGNATURE
All combined:
GAS_COIN_ID=$(iota client gas --json | jq -r '.[0].gasCoinId')
IOTA_ADDRESS=$(iota keytool list --json | jq -r '.[0].iotaAddress')
UNSIGNED_TX_BYTES=$(iota client pay-all-iota --input-coins $GAS_COIN_ID --recipient $IOTA_ADDRESS --gas-budget 2000000 --serialize-unsigned-transaction)
SERIALIZED_SIGNATURE=$(iota keytool sign --address $IOTA_ADDRESS --data $UNSIGNED_TX_BYTES --json | jq -r '.iotaSignature')
iota client execute-signed-tx --tx-bytes $UNSIGNED_TX_BYTES --signatures $SERIALIZED_SIGNATURE
Additional Notes
- This tutorial focuses on signing with a single private key. For more complex signing policies, refer to the Multisig guide.
- If you implement your own signing mechanism, consult the Signatures documentation for accepted specifications.
- The 'flag' is a byte that differentiates signature schemes. See Signatures for supported schemes and their flags.
- The
execute_transaction_block
endpoint accepts a list of signatures, typically requiring only one user signature unless using sponsored transactions. For more information, see Sponsored Transactions.