Skip to content

Commit

Permalink
Add creation methods for different transaction types
Browse files Browse the repository at this point in the history
- Introduces a TransactionBuilder class that is instantiated from the client and receives the client's networkId and blockchain proxy, to access the current head block number, allowing users to omit these when creating transactions.
- Adds a `sign(keyPair)` method on `Transaction` that automatically signs the transaction with the correct signature(s) format.
- Add a constructor function to `Transaction` to allow creating arbitrary transactions which are not yet covered by the transaction builder.
  • Loading branch information
sisou committed Feb 19, 2023
1 parent 9a654c6 commit 10d0b2d
Show file tree
Hide file tree
Showing 6 changed files with 518 additions and 24 deletions.
1 change: 1 addition & 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 web-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ nimiq-keys = { path = "../keys" }
nimiq-network-interface = { path = "../network-interface" }
nimiq-primitives = {path = "../primitives", features = ["coin", "networks"]}
nimiq-transaction = { path = "../primitives/transaction" }
nimiq-transaction-builder = { path = "../transaction-builder" }

[dependencies.nimiq]
package = "nimiq-lib"
Expand Down
103 changes: 96 additions & 7 deletions web-client/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,115 @@ init().then(async () => {
* @param {string} privateKey
* @param {string} recipient
* @param {number} amount
* @param {string} [message]
* @param {number} [fee]
* @returns {Promise<string>}
*/
window.sendTransaction = async (privateKey, recipient, amount, fee = 0) => {
window.sendBasicTransaction = async (privateKey, recipient, amount, message, fee = 0) => {
if (!client.isEstablished()) {
throw new Error('Consensus not yet established');
}

const keyPair = Nimiq.KeyPair.derive(Nimiq.PrivateKey.fromHex(privateKey));

const transaction = Nimiq.Transaction.newBasicTransaction(
const transactionBuilder = client.transactionBuilder();

let transaction;
if (message) {
const messageBytes = new TextEncoder().encode(message);

transaction = transactionBuilder.newBasicWithData(
keyPair.toAddress(),
Nimiq.Address.fromString(recipient),
messageBytes,
BigInt(amount),
BigInt(fee),
).sign(keyPair);
} else {
transaction = transactionBuilder.newBasic(
keyPair.toAddress(),
Nimiq.Address.fromString(recipient),
BigInt(amount),
BigInt(fee),
).sign(keyPair);
}

await client.sendTransaction(transaction);
return transaction.hash();
}

/**
* @param {string} privateKey
* @param {string} delegation
* @param {number} amount
* @param {number} [fee]
* @returns {Promise<string>}
*/
window.sendCreateStakerTransaction = async (privateKey, delegation, amount, fee = 0) => {
if (!client.isEstablished()) {
throw new Error('Consensus not yet established');
}

const keyPair = Nimiq.KeyPair.derive(Nimiq.PrivateKey.fromHex(privateKey));

const transactionBuilder = client.transactionBuilder();

const transaction = transactionBuilder.newCreateStaker(
keyPair.toAddress(),
Nimiq.Address.fromString(recipient),
Nimiq.Address.fromString(delegation),
BigInt(amount),
BigInt(fee),
client.blockNumber(),
client.networkId,
);
).sign(keyPair);

keyPair.signTransaction(transaction);
await client.sendTransaction(transaction);
return transaction.hash();
}

/**
* @param {string} privateKey
* @param {string} newDelegation
* @param {number} [fee]
* @returns {Promise<string>}
*/
window.sendUpdateStakerTransaction = async (privateKey, newDelegation, fee = 0) => {
if (!client.isEstablished()) {
throw new Error('Consensus not yet established');
}

const keyPair = Nimiq.KeyPair.derive(Nimiq.PrivateKey.fromHex(privateKey));

const transactionBuilder = client.transactionBuilder();

const transaction = transactionBuilder.newUpdateStaker(
keyPair.toAddress(),
Nimiq.Address.fromString(newDelegation),
BigInt(fee),
).sign(keyPair);

await client.sendTransaction(transaction);
return transaction.hash();
}

/**
* @param {string} privateKey
* @param {number} amount
* @param {number} [fee]
* @returns {Promise<string>}
*/
window.sendUnstakeTransaction = async (privateKey, amount, fee = 0) => {
if (!client.isEstablished()) {
throw new Error('Consensus not yet established');
}

const keyPair = Nimiq.KeyPair.derive(Nimiq.PrivateKey.fromHex(privateKey));

const transactionBuilder = client.transactionBuilder();

const transaction = transactionBuilder.newUnstake(
keyPair.toAddress(),
BigInt(amount),
BigInt(fee),
).sign(keyPair);

await client.sendTransaction(transaction);
return transaction.hash();
Expand Down
15 changes: 12 additions & 3 deletions web-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use nimiq_network_interface::{

use crate::peer_info::PeerInfo;
use crate::transaction::Transaction;
use crate::transaction_builder::TransactionBuilder;
use crate::utils::{from_network_id, to_network_id};

mod address;
Expand All @@ -38,6 +39,7 @@ mod public_key;
mod signature;
mod signature_proof;
mod transaction;
mod transaction_builder;
mod utils;

/// Use this to provide initialization-time configuration to the Client.
Expand Down Expand Up @@ -98,13 +100,14 @@ impl ClientConfiguration {

/// Nimiq Albatross client that runs in browsers via WASM and is exposed to Javascript.
///
/// Usage:
/// ### Usage:
///
/// ```js
/// import init, * as Nimiq from "./pkg/nimiq_web_client.js";
///
/// init().then(async () => {
/// const configBuilder = Nimiq.ClientConfiguration.builder();
/// const client = await configBuilder.instantiateClient();
/// const config = new Nimiq.ClientConfiguration();
/// const client = await config.instantiateClient();
/// // ...
/// });
/// ```
Expand Down Expand Up @@ -338,6 +341,12 @@ impl Client {
self.inner.blockchain_head().block_number()
}

/// Instantiates a transaction builder class that provides helper methods to create transactions.
#[wasm_bindgen(js_name = transactionBuilder)]
pub fn transaction_builder(&self) -> TransactionBuilder {
TransactionBuilder::new(self.network_id, self.inner.blockchain())
}

/// Sends a transaction to the network. This method does not check if the
/// transaction gets included into a block.
///
Expand Down
158 changes: 144 additions & 14 deletions web-client/src/transaction.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use nimiq_transaction::TransactionFlags;
use wasm_bindgen::prelude::*;

use beserial::Serialize;
use nimiq_hash::{Blake2bHash, Hash};
use nimiq_primitives::coin::Coin;
use nimiq_primitives::{account::AccountType, coin::Coin};
use nimiq_transaction_builder::TransactionProofBuilder;

use crate::address::Address;
use crate::key_pair::KeyPair;
use crate::utils::{from_network_id, to_network_id};

/// Transactions describe a transfer of value, usually from the sender to the recipient.
Expand All @@ -14,38 +17,165 @@ use crate::utils::{from_network_id, to_network_id};
///
/// Transactions require a valid signature proof over their serialized content.
/// Furthermore, transactions are only valid for 2 hours after their validity-start block height.
#[wasm_bindgen]
#[wasm_bindgen(inspectable)]
pub struct Transaction {
inner: nimiq_transaction::Transaction,
}

#[wasm_bindgen]
impl Transaction {
/// Creates a basic transaction that simply transfers `value` amount of luna (NIM's smallest unit)
/// from the sender to the recipient.
/// Creates a new unsigned transaction that transfers `value` amount of luna (NIM's smallest unit)
/// from the sender to the recipient, where both sender and recipient can be any account type,
/// and custom extra data can be added to the transaction.
///
/// ### Basic transactions
/// If both the sender and recipient types are omitted or `0` and both data and flags are empty,
/// a smaller basic transaction is created.
///
/// ### Extended transactions
/// If no flags are given, but sender type is not basic (`0`) or data is set, an extended
/// transaction is created.
///
/// ### Contract creation transactions
/// To create a new vesting or HTLC contract, set `flags` to `0b1` and specify the contract
/// type as the `recipient_type`: `1` for vesting, `2` for HTLC. The `data` bytes must have
/// the correct format of contract creation data for the respective contract type.
///
/// The returned transaction is not yet signed. You can sign it e.g. with `KeyPair.signTransaction`.
/// ### Signalling transactions
/// To interact with the staking contract, signalling transaction are often used that do not
/// transfer any value, but instead simply _signal_ a state change, such as changing one's stake
/// from one validator to another. To create such a transaction, set `flags` to `0b10` and
/// populate the `data` bytes accordingly.
///
/// Throws when the numbers given for value and fee do not fit within a u64 or the networkId is unknown.
#[wasm_bindgen(js_name = newBasicTransaction)]
pub fn new_basic_transaction(
/// The returned transaction is not yet signed. You can sign it e.g. with `tx.sign(keyPair)`.
///
/// Throws when an account type is unknown, the numbers given for value and fee do not fit
/// within a u64 or the networkId is unknown. Also throws when no data or recipient type is
/// given for contract creation transactions, or no data is given for signalling transactions.
#[wasm_bindgen(constructor)]
pub fn new(
sender: &Address,
sender_type: Option<u8>,
recipient: &Address,
recipient_type: Option<u8>,
value: u64,
fee: u64,
fee: Option<u64>,
data: Option<Vec<u8>>,
flags: Option<u8>,
validity_start_height: u32,
network_id: u8,
) -> Result<Transaction, JsError> {
Ok(Transaction::from_native(
nimiq_transaction::Transaction::new_basic(
let flags = TransactionFlags::try_from(flags.unwrap_or(0b0))?;

let tx = if flags.is_empty() {
// This also creates basic transactions
nimiq_transaction::Transaction::new_extended(
sender.native_ref().clone(),
AccountType::try_from(sender_type.unwrap_or(0))?,
recipient.native_ref().clone(),
AccountType::try_from(recipient_type.unwrap_or(0))?,
Coin::try_from(value)?,
Coin::try_from(fee.unwrap_or(0))?,
data.unwrap_or(Vec::new()),
validity_start_height,
to_network_id(network_id)?,
)
} else if flags.contains(TransactionFlags::CONTRACT_CREATION) {
nimiq_transaction::Transaction::new_contract_creation(
data.unwrap_throw(),
sender.native_ref().clone(),
AccountType::try_from(sender_type.unwrap_or(0))?,
AccountType::try_from(recipient_type.unwrap_throw())?,
Coin::try_from(value)?,
Coin::try_from(fee.unwrap_or(0))?,
validity_start_height,
to_network_id(network_id)?,
)
} else if flags.contains(TransactionFlags::SIGNALLING) {
nimiq_transaction::Transaction::new_signalling(
sender.native_ref().clone(),
AccountType::try_from(sender_type.unwrap_or(0))?,
recipient.native_ref().clone(),
AccountType::try_from(recipient_type.unwrap_or(3))?,
Coin::try_from(value)?,
Coin::try_from(fee)?,
Coin::try_from(fee.unwrap_or(0))?,
data.unwrap_throw(),
validity_start_height,
to_network_id(network_id)?,
),
))
)
} else {
return Err(JsError::new("Invalid flags"));
};

Ok(Transaction::from_native(tx))
}

/// Signs the transaction with the provided key pair. Automatically determines the format
/// of the signature proof required for the transaction.
///
/// ### Limitations
/// - HTLC redemption is not supported and will throw.
/// - Validator deletion transactions are not and cannot be supported.
/// - For transaction to the staking contract, both signatures are made with the same keypair,
/// so it is not possible to interact with a staker that is different from the sender address
/// or using a different cold or signing key for validator transactions.
pub fn sign(&self, key_pair: &KeyPair) -> Result<Transaction, JsError> {
let proof_builder = TransactionProofBuilder::new(self.native_ref().clone());
let signed_transaction = match proof_builder {
TransactionProofBuilder::Basic(mut builder) => {
builder.sign_with_key_pair(key_pair.native_ref());
builder.generate().unwrap()
}
TransactionProofBuilder::Vesting(mut builder) => {
builder.sign_with_key_pair(key_pair.native_ref());
builder.generate().unwrap()
}
TransactionProofBuilder::Htlc(mut _builder) => {
// TODO: Create a separate HTLC signing method that takes the type of proof as an argument
return Err(JsError::new(
"HTLC redemption transactions are not supported",
));

// // Redeem
// let sig = builder.signature_with_key_pair(key_pair);
// builder.regular_transfer(hash_algorithm, pre_image, hash_count, hash_root, sig);

// // Refund
// let sig = builder.signature_with_key_pair(key_pair);
// builder.timeout_resolve(sig);

// // Early resolve
// builder.early_resolve(htlc_sender_signature, htlc_recipient_signature);

// // Sign early
// let sig = builder.signature_with_key_pair(key_pair);
// return Ok(sig);

// builder.generate().unwrap()
}
TransactionProofBuilder::OutStaking(mut builder) => {
// There is no way to distinguish between an unstaking and validator-deletion transaction
// from the transaction itself.
// Validator-deletion transactions are thus not supported.
// builder.delete_validator(key_pair.native_ref());

builder.unstake(key_pair.native_ref());
builder.generate().unwrap()
}
TransactionProofBuilder::InStaking(mut builder) => {
// It is possible to add an additional argument `secondary_key_pair: Option<&KeyPair>` with
// https://docs.rs/wasm-bindgen-derive/latest/wasm_bindgen_derive/#optional-arguments.
// TODO: Support signing for differing staker and validator signing & cold keys.
let secondary_key_pair: Option<&KeyPair> = None;

builder.sign_with_key_pair(secondary_key_pair.unwrap_or(key_pair).native_ref());
let mut builder = builder.generate().unwrap().unwrap_basic();
builder.sign_with_key_pair(key_pair.native_ref());
builder.generate().unwrap()
}
};

Ok(Transaction::from_native(signed_transaction))
}

/// Computes the transaction's hash, which is used as its unique identifier on the blockchain.
Expand Down
Loading

0 comments on commit 10d0b2d

Please sign in to comment.