Transparent Transactions
Table of Contents
Constructing Transparent Transactions
The web-wallet will need to support many transactions. As the data that gets submitted to the ledger is most easily constructed from anoma
types, we perform the assembly of the transaction with in WebAssembly using Rust so that we may natively interact with anoma
. The role of wasm in this scenario is to provide two pieces of data to the client (which will handle the broadcasting of the transaction), which are:
hash
- the hash of the transactiondata
- A byte array of the final wrapped and signed transaction
The following outlines how we can construct these transactions before returning them to the client.
Part 1 - Token Transfer Transactions
There are a few steps involved in creating and signing a transaction:
- Create an
anoma::proto::Tx struct
and sign it with a keypair - Wrap Tx with a
anoma::types::transaction::WrapperTx
struct which encrypts the transaction - Create a new
anoma::proto::Tx
with the newWrapperTx
as data, and sign it with a keypair (this will be broadcast to the ledger)
1.1 - Creating the anoma::proto::Tx
struct
The requirements for creating this struct are as follow:
- A pre-built wasm in the form of a byte array (this is loaded in the client as a
Uint8Array
type to pass to the wasm) - A serialized
anoma::types::token::Transfer
object which contains the following:source
- source address derived from keypairtarget
- target addresstoken
- token addressamount
- amount to transfer
- A UTC timestamp. NOTE this is created when calling
proto::Tx::new()
, however, this is incompatible with the wasm in runtime (time
is undefined). Therefore, we need to get a valid timestamp fromjs_sys
:
#![allow(unused)] fn main() { // anoma-lib/src/util.rs pub fn get_timestamp() -> DateTimeUtc { let now = js_sys::Date::new_0(); let year = now.get_utc_full_year() as i32; let month: u32 = now.get_utc_month() + 1; let day: u32 = now.get_utc_date(); let hour: u32 = now.get_utc_hours(); let min: u32 = now.get_utc_minutes(); let sec: u32 = now.get_utc_seconds(); let utc = Utc.ymd(year, month, day).and_hms(hour, min, sec); DateTimeUtc(utc) } }
Creating the types::token::Transfer
struct to pass in as data:
In wasm:
#![allow(unused)] fn main() { // anoma-lib/src/transfer.rs let transfer = token::Transfer { source: source.0, target: target.0, token: token.0.clone(), amount, }; // The data we pass to proto::Tx::new let data = transfer .try_to_vec() .expect("Encoding unsigned transfer shouldn't fail"); }
In Anoma CLI: https://github.com/anoma/anoma/blob/f6e78278608aaef253617885bb7ef95a50057268/apps/src/lib/client/tx.rs#L406-L411
Creating and signing the proto::Tx
struct
In wasm:
#![allow(unused)] fn main() { // anoma-lib/src/types/tx.rs impl Tx { pub fn new(tx_code: Vec<u8>, data: Vec<u8>) -> proto::Tx { proto::Tx { code: tx_code, data: Some(data), timestamp: utils::get_timestamp(), } } } }
NOTE Here we provide a work around to an issue with proto::Tx::new()
in wasm - instead of calling the method directly on Tx
, we create a new implementation that returns a proto::Tx
, with the timestamp being set using js_sys
in order to make this wasm-compatible.
In Anoma CLI: https://github.com/anoma/anoma/blob/f6e78278608aaef253617885bb7ef95a50057268/apps/src/lib/client/tx.rs#L417-L419
1.2 - Creating the anoma::types::transaction::WrapperTx
struct
The requirements for creating this struct are as follows:
- A
transaction::Fee
type, which contains:amount
- the Fee amounttoken
- the address of the token
epoch
- The ID of the epoch from querygas_limit
- This contains au64
value representing the gas limittx
- theproto::Tx
type we created earlier.
In wasm:
#![allow(unused)] fn main() { // anoma-lib/src/types/wrapper.rs transaction::WrapperTx::new( transaction::Fee { amount, token: token.0, }, &keypair, storage::Epoch(u64::from(epoch)), transaction::GasLimit::from(gas_limit), tx, ) }
NOTE Here we can directly invoke WrapperTx::new
, so we only need to concern ourselves with convering the JavaScript-provided values into the appropriate types.
In Anoma CLI: https://github.com/anoma/anoma/blob/f6e78278608aaef253617885bb7ef95a50057268/apps/src/lib/client/tx.rs#L687-L696
1.3 - Create a new Tx
with WrapperTx
and sign it
Here we create a WrapperTx
type, and with that we create a new Tx
type (our wrapped Tx
type) with the WrapperTx
as the data
, and empty vec![]
for code
, and a new timestamp
, and then we sign it.
In wasm:
#![allow(unused)] fn main() { // anoma-lib/src/types/wrapper.rs -> sign() (Tx::new( vec![], transaction::TxType::Wrapper(wrapper_tx) .clone() .try_to_vec().expect("Could not serialize WrapperTx") )).sign(&keypair) }
We can summarize a high-level overview of the entire process from the anoma-lib/src/types/transaction.rs
implementation:
#![allow(unused)] fn main() { let source_keypair = Keypair::deserialize(serialized_keypair)?; let keypair = key::ed25519::Keypair::from_bytes(&source_keypair.to_bytes()) .expect("Could not create keypair from bytes"); let tx = Tx::new( tx_code, data, ).sign(&keypair); let wrapper_tx = WrapperTx::new( token, fee_amount, &keypair, epoch, gas_limit, tx, ); let hash = wrapper_tx.tx_hash.to_string(); let wrapper_tx = WrapperTx::sign(wrapper_tx, &keypair); let bytes = wrapper_tx.to_bytes(); // Return serialized wrapped & signed transaction as bytes with hash // in a tuple: Ok(Transaction { hash, bytes, }) }
In Anoma CLI: https://github.com/anoma/anoma/blob/f6e78278608aaef253617885bb7ef95a50057268/apps/src/lib/client/tx.rs#L810-L814
Part 2 - Initialize Account Transaction
Constructing an Initialize Account transaction follows a similar process to a transfer, however, in addition to providing a tx_init_account
wasm, we need to provide the vp_user
wasm as well, as this is required when constructing the transaction:
#![allow(unused)] fn main() { // anoma-lib/src/account.rs let vp_code: Vec<u8> = vp_code.to_vec(); let keypair = &Keypair::deserialize(serialized_keypair.clone()) .expect("Keypair could not be deserialized"); let public_key = PublicKey::from(keypair.0.public.clone()); let data = InitAccount { public_key, vp_code: vp_code.clone(), }; }
Following this, we will pass data
into to our new transaction as before, along with tx_code
and required values for WrapperTx
, returning the final result in a JsValue
containing the transaction hash and returned byte array.
Submitting Transparent Transactions
See RPC for more information on HTTP and WebSocket RPC interaction with ledger.