Skip to content
17 changes: 17 additions & 0 deletions rust/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 rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ members = [
"chains/tw_ronin",
"chains/tw_solana",
"chains/tw_sui",
"chains/tw_tezos",
"chains/tw_thorchain",
"chains/tw_ton",
"chains/tw_zcash",
Expand Down
19 changes: 19 additions & 0 deletions rust/chains/tw_tezos/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "tw_tezos"
version = "0.1.0"
edition = "2021"

[dependencies]
tw_base58_address = { path = "../../tw_base58_address" }
tw_coin_entry = { path = "../../tw_coin_entry" }
tw_encoding = { path = "../../tw_encoding" }
tw_keypair = { path = "../../tw_keypair" }
tw_hash = { path = "../../tw_hash" }
tw_memory = { path = "../../tw_memory" }
tw_misc = { path = "../../tw_misc", features = ["serde"] }
tw_proto = { path = "../../tw_proto" }
zeroize = "1.8.1"

[dev-dependencies]
tw_coin_entry = { path = "../../tw_coin_entry", features = ["test-utils"] }
tw_number = { path = "../../tw_number", features = ["helpers"] }
160 changes: 160 additions & 0 deletions rust/chains/tw_tezos/src/address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

use std::fmt;
use std::str::FromStr;
use tw_base58_address::Base58Address;
use tw_coin_entry::coin_entry::CoinAddress;
use tw_coin_entry::error::prelude::*;
use tw_encoding::base58::Alphabet;
use tw_hash::blake2::blake2_b;
use tw_hash::sha2::Sha256d;
use tw_hash::H160;
use tw_keypair::{
ecdsa, ed25519,
tw::{PublicKey, PublicKeyType},
};
use tw_memory::Data;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


use crate::forging::forge_public_key_hash;

pub const TEZOS_ADDRESS_SIZE: usize = 23;
pub const TEZOS_ADDRESS_PREFIX_SIZE: usize = 3;
pub const TEZOS_ADDRESS_PUBLIC_KEY_HASH_SIZE: usize = 20;
pub const TEZOS_ADDRESS_CHECKSUM_SIZE: usize = 4;

pub const TEZOS_ADDRESS_SECP256K1_PREFIX: [u8; TEZOS_ADDRESS_PREFIX_SIZE] = [0x06, 0xa1, 0xa1];
pub const TEZOS_ADDRESS_ED25519_PREFIX: [u8; TEZOS_ADDRESS_PREFIX_SIZE] = [0x06, 0xa1, 0x9f];
pub const TEZOS_ADDRESS_OTHER_PREFIX: [u8; TEZOS_ADDRESS_PREFIX_SIZE] = [0x06, 0xa1, 0xa4];
pub const TEZOS_ADDRESS_KT1_PREFIX: [u8; TEZOS_ADDRESS_PREFIX_SIZE] = [0x02, 0x5a, 0x79];

pub const ACCOUNT_ZERO_BYTES: [u8; TEZOS_ADDRESS_SIZE] = [0; TEZOS_ADDRESS_SIZE];

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TezosAddress(Base58Address<TEZOS_ADDRESS_SIZE, TEZOS_ADDRESS_CHECKSUM_SIZE, Sha256d>);

impl TezosAddress {
fn new(public_key_hash: &[u8], prefix: &[u8]) -> AddressResult<TezosAddress> {
if prefix.len() != TEZOS_ADDRESS_PREFIX_SIZE {
return Err(AddressError::InvalidInput);
}
if public_key_hash.len() != TEZOS_ADDRESS_PUBLIC_KEY_HASH_SIZE {
return Err(AddressError::InvalidInput);
}
let mut bytes: Data = Vec::with_capacity(prefix.len() + public_key_hash.len());
bytes.extend_from_slice(prefix);
bytes.extend_from_slice(public_key_hash);
Base58Address::new(&bytes, Alphabet::Bitcoin).map(TezosAddress)
}

pub fn with_public_key(public_key: &PublicKey) -> AddressResult<TezosAddress> {
if public_key.public_key_type() == PublicKeyType::Secp256k1 {
TezosAddress::with_secp256k1_public_key(
public_key
.to_secp256k1()
.ok_or(AddressError::PublicKeyTypeMismatch)?,
)
} else if public_key.public_key_type() == PublicKeyType::Ed25519 {
TezosAddress::with_ed25519_public_key(
public_key
.to_ed25519()
.ok_or(AddressError::PublicKeyTypeMismatch)?,
)
} else {
Err(AddressError::InvalidInput)
}
}

pub fn with_secp256k1_public_key(
public_key: &ecdsa::secp256k1::PublicKey,
) -> AddressResult<TezosAddress> {
let bytes = blake2_b(
public_key.compressed().as_slice(),
TEZOS_ADDRESS_PUBLIC_KEY_HASH_SIZE,
)?;
TezosAddress::new(&bytes, &TEZOS_ADDRESS_SECP256K1_PREFIX)
}

pub fn with_ed25519_public_key(
public_key: &ed25519::sha512::PublicKey,
) -> AddressResult<TezosAddress> {
let bytes = blake2_b(public_key.as_slice(), TEZOS_ADDRESS_PUBLIC_KEY_HASH_SIZE)?;
TezosAddress::new(&bytes, &TEZOS_ADDRESS_ED25519_PREFIX)
}

/// Address bytes excluding the prefix (skip first 3 bytes).
pub fn bytes(&self) -> &[u8] {
&self.0.as_ref()[TEZOS_ADDRESS_PREFIX_SIZE..]
}

/// Returns public key hash associated with the address.
pub fn public_key_hash(&self) -> H160 {
H160::try_from(&self.0.as_ref()[TEZOS_ADDRESS_PREFIX_SIZE..])
.expect("Expected exactly 20 bytes public key hash")
}

pub fn forge(&self) -> AddressResult<Data> {
// normal address
// https://github.com/ecadlabs/taquito/blob/master/packages/taquito-local-forging/src/codec.ts#L183
let prefix = &self.0.bytes[..TEZOS_ADDRESS_PREFIX_SIZE];
if prefix == TEZOS_ADDRESS_SECP256K1_PREFIX
|| prefix == TEZOS_ADDRESS_ED25519_PREFIX
|| prefix == TEZOS_ADDRESS_OTHER_PREFIX
{
let mut forged = vec![0x00];
let forged_public_key_hash =
forge_public_key_hash(&self.to_string()).map_err(|_| AddressError::InvalidInput)?;
forged.extend_from_slice(&forged_public_key_hash);
Ok(forged)
}
// contract address
// https://github.com/ecadlabs/taquito/blob/master/packages/taquito-local-forging/src/codec.ts#L183
else if prefix == TEZOS_ADDRESS_KT1_PREFIX {
let mut forged = vec![0x01];
forged.extend_from_slice(self.bytes());
forged.push(0x00);
Ok(forged)
} else {
Err(AddressError::InvalidInput)
}
}
}

impl Default for TezosAddress {
fn default() -> Self {
Base58Address::new(ACCOUNT_ZERO_BYTES.as_slice(), Alphabet::Bitcoin)
.map(TezosAddress)
.expect("'ACCOUNT_ZERO_BYTES' is expected to be valid address bytes")
}
}

impl CoinAddress for TezosAddress {
#[inline]
fn data(&self) -> Data {
self.bytes().to_vec()
}
}

impl FromStr for TezosAddress {
type Err = AddressError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let base58_addr = Base58Address::<TEZOS_ADDRESS_SIZE, TEZOS_ADDRESS_CHECKSUM_SIZE, Sha256d>::from_str_with_alphabet(s, Alphabet::Bitcoin)?;
let prefix = &base58_addr.bytes[..TEZOS_ADDRESS_PREFIX_SIZE];
if prefix != TEZOS_ADDRESS_SECP256K1_PREFIX
&& prefix != TEZOS_ADDRESS_ED25519_PREFIX
&& prefix != TEZOS_ADDRESS_OTHER_PREFIX
&& prefix != TEZOS_ADDRESS_KT1_PREFIX
{
return Err(AddressError::UnexpectedAddressPrefix);
}
Ok(TezosAddress(base58_addr))
}
}

impl fmt::Display for TezosAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
94 changes: 94 additions & 0 deletions rust/chains/tw_tezos/src/binary_coding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

use tw_encoding::{
base58::{self, Alphabet, CHECKSUM_LEN},
hex, EncodingError,
};
use tw_hash::sha2::sha256_d;
use tw_keypair::{ecdsa, ed25519, KeyPairError};
use tw_memory::Data;
use zeroize::Zeroizing;

use crate::address::TEZOS_ADDRESS_PREFIX_SIZE;

const TEZOS_KEY_PREFIX_SIZE: usize = 4;

pub fn decode_check(input: &str, alphabet: Alphabet) -> Result<Data, EncodingError> {
let data = base58::decode(input, alphabet)?;
if data.len() < CHECKSUM_LEN {
return Err(EncodingError::InvalidInput);
}

let checksum = &data[data.len() - CHECKSUM_LEN..];
let hash = sha256_d(&data[..data.len() - CHECKSUM_LEN]);
if checksum != &hash[..CHECKSUM_LEN] {
return Err(EncodingError::InvalidInput);
}
Ok(Data::from(&data[..data.len() - CHECKSUM_LEN]))
}

pub fn encode_check(data: &[u8], alphabet: Alphabet) -> String {
let hash = sha256_d(data);
let mut to_be_encoded = Vec::from(data);
to_be_encoded.extend_from_slice(&hash[..CHECKSUM_LEN]);
base58::encode(&to_be_encoded, alphabet)
}

pub fn base58_to_hex(input: &str, prefix_length: usize) -> Result<String, EncodingError> {
let decoded = match decode_check(input, Alphabet::Bitcoin) {
Ok(d) => d,
Err(_) => return Err(EncodingError::InvalidInput),
};

if decoded.len() < prefix_length {
return Err(EncodingError::InvalidInput);
}

Ok(hex::encode(&decoded[prefix_length..], false))
}

pub fn parse_public_key(public_key: &str) -> Result<tw_keypair::tw::PublicKey, KeyPairError> {
let decoded =
decode_check(public_key, Alphabet::Bitcoin).map_err(|_| KeyPairError::InvalidPublicKey)?;

let ed25519_prefix = [13, 15, 37, 217];
let secp256k1_prefix = [3, 254, 226, 86];

if decoded.starts_with(&ed25519_prefix) {
if decoded.len() != 32 + TEZOS_KEY_PREFIX_SIZE {
return Err(KeyPairError::InvalidPublicKey);
}
Ok(tw_keypair::tw::PublicKey::Ed25519(
ed25519::sha512::PublicKey::try_from(&decoded[TEZOS_KEY_PREFIX_SIZE..])?,
))
} else if decoded.starts_with(&secp256k1_prefix) {
if decoded.len() != 33 + TEZOS_KEY_PREFIX_SIZE {
return Err(KeyPairError::InvalidPublicKey);
}
Ok(tw_keypair::tw::PublicKey::Secp256k1(
ecdsa::secp256k1::PublicKey::try_from(&decoded[TEZOS_KEY_PREFIX_SIZE..])?,
))
} else {
Err(KeyPairError::InvalidPublicKey)
}
}

pub fn parse_private_key(private_key: &str) -> Result<tw_keypair::tw::PrivateKey, KeyPairError> {
let decoded = Zeroizing::new(
decode_check(private_key, Alphabet::Bitcoin).map_err(|_| KeyPairError::InvalidSecretKey)?,
);

if decoded.len() != 32 + TEZOS_KEY_PREFIX_SIZE {
return Err(KeyPairError::InvalidSecretKey);
}

tw_keypair::tw::PrivateKey::new((decoded[TEZOS_KEY_PREFIX_SIZE..]).to_vec())
}

pub fn encode_prefix(address: &str, forged: &mut Data) -> Result<(), EncodingError> {
let decoded = decode_check(address, Alphabet::Bitcoin)?;
forged.extend_from_slice(&decoded[TEZOS_ADDRESS_PREFIX_SIZE..]);
Ok(())
}
Loading
Loading