From 115184a8c712e3432cc960273278780ddc1b768a Mon Sep 17 00:00:00 2001 From: Antoni Dikov Date: Wed, 7 Apr 2021 23:21:10 +0300 Subject: [PATCH] Added hex_to_bech32 and bech32_to_hex methods (#471) * Added hex_to_bech32 and bech32_to_hex methods in client * Added spec documentation * Added bindings bech32 to hex and hex to bech32 methods * fmt fix * added promisify for hexToBech32 * handle serde_json errors, add convert test * fixed js binding * fix test * update output id * add changes for covetor Co-authored-by: Thoralf-M --- .changes/bech32-hex-conversion.md | 5 ++ bindings/nodejs/README.md | 23 +++++- bindings/nodejs/lib/index.d.ts | 4 +- bindings/nodejs/lib/index.js | 1 + .../nodejs/native/src/classes/client/api.rs | 73 +++++++++---------- .../nodejs/native/src/classes/client/mod.rs | 38 +++++++++- bindings/nodejs/native/src/lib.rs | 8 ++ bindings/nodejs/package-lock.json | 2 +- bindings/nodejs/tests/client.js | 7 ++ bindings/python/README.md | 21 ++++++ .../native/src/client/high_level_api.rs | 8 ++ fixtures/test_vectors.json | 2 +- iota-client/src/client.rs | 23 +++++- iota-client/src/error.rs | 3 + specs/iota-rs-ENGINEERING-SPEC-0000.md | 31 ++++++++ 15 files changed, 203 insertions(+), 46 deletions(-) create mode 100644 .changes/bech32-hex-conversion.md diff --git a/.changes/bech32-hex-conversion.md b/.changes/bech32-hex-conversion.md new file mode 100644 index 000000000..b7d1ed534 --- /dev/null +++ b/.changes/bech32-hex-conversion.md @@ -0,0 +1,5 @@ +--- +"nodejs-binding": minor +--- + +Added functions to convert addresses from bech32 to hex and vice versa. \ No newline at end of file diff --git a/bindings/nodejs/README.md b/bindings/nodejs/README.md index e7b6a8008..67e24bb02 100644 --- a/bindings/nodejs/README.md +++ b/bindings/nodejs/README.md @@ -242,6 +242,27 @@ Get the balance in iotas for the given addresses. **Returns** A promise resolving to the list of `{ address, balance }` pairs. +#### bech32ToHex(bech32) + +Returns a parsed hex String from bech32. + +| Param | Type | Description | +| ------- | ------------------- | ------------------------- | +| bech32 | string | The address Bech32 string | + +**Returns** A String + +#### hexToBech32(hex, bech32_hrp (optional)) + +Returns a parsed bech32 String from hex. + +| Param | Type | Description | +| ----------- | ------------------- | ------------------------- | +| bech32 | string | The address Bech32 string | +| bech32_hrp | string | The Bech32 hrp string | + +**Returns** A String + #### isAddressValid(address: string): boolean Checks if a given address is valid. @@ -669,7 +690,7 @@ Sets the initial address index. Defaults to 0 if the function isn't called. #### gapLimit(amount): BalanceGetter -Sets the gapLimit to specify how many addresses will be checked each round. +Sets the gapLimit to specify how many addresses will be checked each round. If gap_limit amount of addresses in a row have no balance the BalanceGetter will return. Defaults to 20 if the function isn't called. | Param | Type | Description | diff --git a/bindings/nodejs/lib/index.d.ts b/bindings/nodejs/lib/index.d.ts index b4569bf8a..b3d2a0bff 100644 --- a/bindings/nodejs/lib/index.d.ts +++ b/bindings/nodejs/lib/index.d.ts @@ -81,7 +81,7 @@ export declare class Client { subscriber(): TopicSubscriber message(): MessageSender getUnspentAddress(seed: string): UnspentAddressGetter - getAddresses(seed: string): AddressGetter + getAddresses(seed: string): Promise findMessages(indexationKeys: string[], messageIds: string[]): Promise getBalance(seed: string): BalanceGetter getAddressBalances(addresses: string[]): Promise @@ -97,6 +97,8 @@ export declare class Client { findOutputs(outputIds: string[], addresses: string[]): Promise getAddressOutputs(address: string, options?: AddressOutputsOptions): Promise getAddressBalance(address: string): Promise + bech32ToHex(address: string): string + hexToBech32(address: string, bech32_hrp?: string): Promise isAddressValid(address: string): boolean getMilestone(index: number): Promise getMilestoneUtxoChanges(index: number): Promise diff --git a/bindings/nodejs/lib/index.js b/bindings/nodejs/lib/index.js index 4781d5316..5b739615d 100644 --- a/bindings/nodejs/lib/index.js +++ b/bindings/nodejs/lib/index.js @@ -112,6 +112,7 @@ Client.prototype.retryUntilIncluded = function (msg_id, interval, maxAttempts) { } Client.prototype.reattach = promisify(Client.prototype.reattach) Client.prototype.promote = promisify(Client.prototype.promote) +Client.prototype.hexToBech32 = promisify(Client.prototype.hexToBech32) const messageGetterIndexSetter = promisify(MessageGetter.prototype.index) MessageGetter.prototype.index = function (index) { diff --git a/bindings/nodejs/native/src/classes/client/api.rs b/bindings/nodejs/native/src/classes/client/api.rs index 57264f648..21e8ebd66 100644 --- a/bindings/nodejs/native/src/classes/client/api.rs +++ b/bindings/nodejs/native/src/classes/client/api.rs @@ -74,6 +74,7 @@ pub(crate) enum Api { RetryUntilIncluded(MessageId, Option, Option), Reattach(MessageId), Promote(MessageId), + HexToBech32(String, Option), } pub(crate) struct ClientTask { @@ -90,7 +91,7 @@ impl Task for ClientTask { fn perform(&self) -> Result { crate::block_on(crate::convert_async_panics(|| async move { let client = crate::get_client(&self.client_id); - let client = client.read().unwrap(); + let client = client.read().expect("Failed to read client"); let res = match &self.api { // High level API Api::Send { @@ -138,8 +139,7 @@ impl Task for ClientTask { serde_json::to_string(&MessageWrapper { message_id: message.id().0, message, - }) - .unwrap() + })? } Api::GetUnspentAddress { seed, @@ -154,7 +154,7 @@ impl Task for ClientTask { getter = getter.with_initial_address_index(*initial_address_index); } let (address, index) = getter.get().await?; - serde_json::to_string(&(address, index)).unwrap() + serde_json::to_string(&(address, index))? } Api::GetAddresses { seed, @@ -175,7 +175,7 @@ impl Task for ClientTask { } let addresses = getter.finish().await?; - serde_json::to_string(&addresses).unwrap() + serde_json::to_string(&addresses)? } Api::FindMessages { indexation_keys, @@ -189,7 +189,7 @@ impl Task for ClientTask { message, }) .collect(); - serde_json::to_string(&message_wrappers).unwrap() + serde_json::to_string(&message_wrappers)? } Api::GetBalance { seed, @@ -208,20 +208,20 @@ impl Task for ClientTask { getter = getter.with_gap_limit(*gap_limit); } let balance = getter.finish().await?; - serde_json::to_string(&balance).unwrap() + serde_json::to_string(&balance)? } Api::GetAddressBalances(bech32_addresses) => { let balances = client.get_address_balances(&bech32_addresses[..]).await?; let balances: Vec = balances.into_iter().map(|b| b.into()).collect(); - serde_json::to_string(&balances).unwrap() + serde_json::to_string(&balances)? } // Node APIs - Api::GetInfo => serde_json::to_string(&client.get_info().await?).unwrap(), - Api::GetNetworkInfo => serde_json::to_string(&client.get_network_info().await?).unwrap(), - Api::GetPeers => serde_json::to_string(&client.get_peers().await?).unwrap(), + Api::GetInfo => serde_json::to_string(&client.get_info().await?)?, + Api::GetNetworkInfo => serde_json::to_string(&client.get_network_info().await?)?, + Api::GetPeers => serde_json::to_string(&client.get_peers().await?)?, Api::GetTips => { let tips = client.get_tips().await?; - serde_json::to_string(&tips).unwrap() + serde_json::to_string(&tips)? } Api::PostMessage(message) => { let parent_msg_ids = match message.parents.as_ref() { @@ -243,82 +243,79 @@ impl Task for ClientTask { .with_payload(message.payload.clone().try_into()?) .finish()?; let message = client.post_message(&message).await?; - serde_json::to_string(&message).unwrap() + serde_json::to_string(&message)? } Api::GetMessagesByIndexation(index) => { let messages = client.get_message().index(index).await?; - serde_json::to_string(&messages).unwrap() + serde_json::to_string(&messages)? } Api::GetMessage(id) => { let message = client.get_message().data(&id).await?; serde_json::to_string(&MessageWrapper { message_id: message.id().0, message, - }) - .unwrap() + })? } Api::GetMessageMetadata(id) => { let metadata = client.get_message().metadata(&id).await?; - serde_json::to_string(&metadata).unwrap() + serde_json::to_string(&metadata)? } Api::GetRawMessage(id) => client.get_message().raw(&id).await?, Api::GetMessageChildren(id) => { let messages = client.get_message().children(&id).await?; - serde_json::to_string(&messages).unwrap() + serde_json::to_string(&messages)? } Api::GetOutput(id) => { let output = client.get_output(id).await?; let output: super::OutputMetadataDto = output.into(); - serde_json::to_string(&output).unwrap() + serde_json::to_string(&output)? } Api::FindOutputs { outputs, addresses } => { let outputs = client.find_outputs(outputs, &addresses[..]).await?; let outputs: Vec = outputs.into_iter().map(|o| o.into()).collect(); - serde_json::to_string(&outputs).unwrap() + serde_json::to_string(&outputs)? } Api::GetAddressBalance(address) => { let balance = client.get_address().balance(address).await?; - serde_json::to_string(&balance).unwrap() + serde_json::to_string(&balance)? } Api::GetAddressOutputs(address, options) => { let output_ids = client.get_address().outputs(address, options.clone()).await?; - serde_json::to_string(&output_ids).unwrap() + serde_json::to_string(&output_ids)? } Api::GetMilestone(index) => { let milestone = client.get_milestone(*index).await?; - serde_json::to_string(&milestone).unwrap() + serde_json::to_string(&milestone)? } Api::GetMilestoneUtxoChanges(index) => { let milestone_utxo_changes = client.get_milestone_utxo_changes(*index).await?; - serde_json::to_string(&milestone_utxo_changes).unwrap() + serde_json::to_string(&milestone_utxo_changes)? } Api::GetReceipts() => { let receipts = client.get_receipts().await?; - serde_json::to_string(&receipts).unwrap() + serde_json::to_string(&receipts)? } Api::GetReceiptsMigratedAt(index) => { let receipts = client.get_receipts_migrated_at(*index).await?; - serde_json::to_string(&receipts).unwrap() + serde_json::to_string(&receipts)? } Api::GetTreasury() => { let treasury = client.get_treasury().await?; - serde_json::to_string(&treasury).unwrap() + serde_json::to_string(&treasury)? } Api::GetIncludedMessage(transaction_id) => { let message = client.get_included_message(&*transaction_id).await?; serde_json::to_string(&MessageWrapper { message_id: message.id().0, message, - }) - .unwrap() + })? } Api::Retry(message_id) => { let message = client.retry(message_id).await?; serde_json::to_string(&MessageWrapper { message_id: message.0, message: message.1, - }) - .unwrap() + })? } Api::RetryUntilIncluded(message_id, interval, max_attempts) => { let messages = client @@ -331,25 +328,27 @@ impl Task for ClientTask { message_id: msg.0, message: msg.1, }) - .unwrap() }) - .collect() + .collect::>()? } Api::Reattach(message_id) => { let message = client.reattach(message_id).await?; serde_json::to_string(&MessageWrapper { message: message.1, message_id: message.0, - }) - .unwrap() + })? } Api::Promote(message_id) => { let message = client.promote(message_id).await?; serde_json::to_string(&MessageWrapper { message: message.1, message_id: message.0, - }) - .unwrap() + })? + } + Api::HexToBech32(hex, bech32_hrp) => { + let opt = bech32_hrp.as_ref().map(|opt| opt.as_str()); + let bech32 = client.hex_to_bech32(hex, opt).await?; + serde_json::to_string(&bech32)? } }; Ok(res) diff --git a/bindings/nodejs/native/src/classes/client/mod.rs b/bindings/nodejs/native/src/classes/client/mod.rs index 4f4453a0e..8aed1bf69 100644 --- a/bindings/nodejs/native/src/classes/client/mod.rs +++ b/bindings/nodejs/native/src/classes/client/mod.rs @@ -3,7 +3,7 @@ use iota::{ message::prelude::{Address, MessageId, TransactionId, UtxoInput}, - AddressOutputsOptions, OutputType, Seed, + AddressOutputsOptions, Client, OutputType, Seed, }; use neon::prelude::*; use serde::Deserialize; @@ -595,10 +595,40 @@ declare_types! { Ok(cx.undefined().upcast()) } - method isAddressValid(mut cx) -> JsResult { + method bech32ToHex(mut cx) { + let bech32 = cx.argument::(0)?.value(); + let hex = Client::bech32_to_hex(bech32.as_str()).unwrap(); + Ok(cx.string(hex).upcast()) + } + + method hexToBech32(mut cx) { + let hex = cx.argument::(0)?.value(); + let bech32_hrp: Option = match cx.argument_opt(1) { + Some(arg) => { + Some(arg.downcast::().or_throw(&mut cx)?.value()) + }, + None => Default::default(), + }; + + let cb = cx.argument::(cx.len()-1)?; + { + let this = cx.this(); + let guard = cx.lock(); + let id = &this.borrow(&guard).0; + let client_task = ClientTask { + client_id: id.clone(), + api: Api::HexToBech32(hex, bech32_hrp), + }; + client_task.schedule(cb); + } + + Ok(cx.undefined().upcast()) + } + + method isAddressValid(mut cx) { let address = cx.argument::(0)?.value(); - let b = cx.boolean(Api::IsAddressValid(address.as_str())); - Ok(b) + let is_valid = Client::is_address_valid(address.as_str()); + Ok(cx.boolean(is_valid).upcast()) } } } diff --git a/bindings/nodejs/native/src/lib.rs b/bindings/nodejs/native/src/lib.rs index bed12685c..903471317 100644 --- a/bindings/nodejs/native/src/lib.rs +++ b/bindings/nodejs/native/src/lib.rs @@ -36,6 +36,8 @@ pub enum Error { Panic(String), #[error("`{0}`")] Message(iota::message::Error), + #[error("`{0}`")] + SerdeJson(serde_json::Error), } impl From for Error { @@ -44,6 +46,12 @@ impl From for Error { } } +impl From for Error { + fn from(error: serde_json::Error) -> Self { + Self::SerdeJson(error) + } +} + pub(crate) fn block_on(cb: C) -> C::Output { static INSTANCE: OnceCell> = OnceCell::new(); let runtime = INSTANCE.get_or_init(|| Mutex::new(Runtime::new().unwrap())); diff --git a/bindings/nodejs/package-lock.json b/bindings/nodejs/package-lock.json index 7118f21ce..53129de83 100644 --- a/bindings/nodejs/package-lock.json +++ b/bindings/nodejs/package-lock.json @@ -1,6 +1,6 @@ { "name": "@iota/client", - "version": "0.0.0", + "version": "0.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/bindings/nodejs/tests/client.js b/bindings/nodejs/tests/client.js index 3d07b05f6..cf6e532be 100644 --- a/bindings/nodejs/tests/client.js +++ b/bindings/nodejs/tests/client.js @@ -36,6 +36,13 @@ describe('Client', () => { addresses.forEach(assertAddress) }) + it('convert address', async () => { + const address = "atoi1qpnrumvaex24dy0duulp4q07lpa00w20ze6jfd0xly422kdcjxzakzsz5kf" + let hexAddress = client.bech32ToHex(address) + let bech32Address = await client.hexToBech32(hexAddress, "atoi") + assert.strictEqual(address, bech32Address) + }) + it('sends an indexation message with the high level API', async () => { const message = await client .message() diff --git a/bindings/python/README.md b/bindings/python/README.md index f9cb409ae..a0684d8f3 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -464,6 +464,27 @@ Get the balance in iotas for the given addresses. **Returns** the list of [AddressBalancePair](#addressbalancepair). +#### bech32_to_hex(bech32) + +Returns a parsed hex String from bech32. + +| Param | Type | Default | Description | +| ------- | ------------------- | ---------------------- | ------------------------- | +| bech32 | string | undefined | The address Bech32 string | + +**Returns** A String + +#### hex_to_bech32(hex, bech32_hrp (optional)) + +Returns a parsed bech32 String from hex. + +| Param | Type | Default | Description | +| ----------- | ------------------- | ---------------------- | ------------------------- | +| bech32 | string | undefined | The address Bech32 string | +| bech32_hrp | string | undefined | The Bech32 hrp string | + +**Returns** A String + #### is_address_valid(address): bool Checks if a given address is valid. diff --git a/bindings/python/native/src/client/high_level_api.rs b/bindings/python/native/src/client/high_level_api.rs index 546af7d09..2a54269d5 100644 --- a/bindings/python/native/src/client/high_level_api.rs +++ b/bindings/python/native/src/client/high_level_api.rs @@ -289,6 +289,14 @@ impl Client { }) .collect()) } + fn bech32_to_hex(&self, hex: &str) -> Result { + Ok(iota::Client::bech32_to_hex(hex)?) + } + fn hex_to_bech32(&self, hex: &str, bech32_hrp: Option<&str>) -> Result { + Ok(crate::block_on(async { + self.client.hex_to_bech32(hex, bech32_hrp).await + })?) + } fn is_address_valid(&self, address: &str) -> bool { iota::Client::is_address_valid(address) } diff --git a/fixtures/test_vectors.json b/fixtures/test_vectors.json index 96263a41e..a919785b8 100644 --- a/fixtures/test_vectors.json +++ b/fixtures/test_vectors.json @@ -31,7 +31,7 @@ ] }, "UTXOINPUT": [ - "00000000000000000000000000000000000000000000000000000000000000000000" + "7702ea0f2cd6af3206b894c3f2fe4362b23f0f4828857d31e733103b09db25840000" ], "MESSAGE_ID": [ "f541e53e98ccffdd94036f08af07f9eb060449ee7838c2279f230acc933de6d8" diff --git a/iota-client/src/client.rs b/iota-client/src/client.rs index 5932fce16..3bf2a5b95 100644 --- a/iota-client/src/client.rs +++ b/iota-client/src/client.rs @@ -9,7 +9,9 @@ use crate::{ node::*, }; use bee_common::packable::Packable; -use bee_message::prelude::{Address, Message, MessageBuilder, MessageId, Parents, TransactionId, UtxoInput}; +use bee_message::prelude::{ + Address, Ed25519Address, Message, MessageBuilder, MessageId, Parents, TransactionId, UtxoInput, +}; use bee_pow::providers::{MinerBuilder, Provider as PowProvider, ProviderBuilder as PowProviderBuilder}; use bee_rest_api::types::{ dtos::{MessageDto, PeerDto, ReceiptDto}, @@ -1109,6 +1111,25 @@ impl Client { Ok(address_balance_pairs) } + /// Transforms bech32 to hex + pub fn bech32_to_hex(bech32: &str) -> crate::Result { + let address = Address::try_from_bech32(bech32)?; + if let Address::Ed25519(ed) = address { + return Ok(ed.to_string()); + } + + Err(crate::Error::FailedToParseBech32ToHex) + } + + /// Transforms hex to bech32 + pub async fn hex_to_bech32(&self, hex: &str, bech32_hrp: Option<&str>) -> crate::Result { + let address: Ed25519Address = hex.parse::()?; + match bech32_hrp { + Some(hrp) => Ok(Address::Ed25519(address).to_bech32(hrp)), + None => Ok(Address::Ed25519(address).to_bech32(self.get_bech32_hrp().await?.as_str())), + } + } + /// Returns a valid Address parsed from a String. pub fn parse_bech32_address(address: &str) -> crate::Result
{ Ok(Address::try_from_bech32(address)?) diff --git a/iota-client/src/error.rs b/iota-client/src/error.rs index 9ba544134..0cf27ee7b 100644 --- a/iota-client/src/error.rs +++ b/iota-client/src/error.rs @@ -119,4 +119,7 @@ pub enum Error { /// Output Error #[error("Output error: {0}")] OutputError(&'static str), + /// Error when parsing from bech32 to hex + #[error("Failed to parse bech32 to hex")] + FailedToParseBech32ToHex, } diff --git a/specs/iota-rs-ENGINEERING-SPEC-0000.md b/specs/iota-rs-ENGINEERING-SPEC-0000.md index 17532c35f..306ffde14 100644 --- a/specs/iota-rs-ENGINEERING-SPEC-0000.md +++ b/specs/iota-rs-ENGINEERING-SPEC-0000.md @@ -14,6 +14,8 @@ * [`get_addresses`](#get_addresses) * [`get_balance`](#get_balance) * [`get_address_balances`](#get_address_balances) + * [`bech32_to_hex`](#bech32_to_hex) + * [`hex_to_bech32`](#hex_to_bech32) * [`parse_bech32_address`](#parse_bech32_address) * [`is_address_valid`](#is_address_valid) * [`subscriber`](#subscriber) @@ -274,6 +276,35 @@ Following are the steps for implementing this method: * Get latest balance for the provided address using [`find_outputs()`](#find_outputs) with addresses as parameter; * Return the list of Output which contains corresponding pairs of address and balance. +## `bech32_to_hex()` + +Returns a parsed hex String from bech32. + +### Parameters + +| Parameter | Required | Type | Definition | +| - | - | - | - | +| **bech32** | ✔ | [String] | Bech32 encoded address. | + +### Return + +Parsed [String]. + +## `hex_to_bech32()` + +Returns a parsed bech32 String from hex. + +### Parameters + +| Parameter | Required | Type | Definition | +| - | - | - | - | +| **hex** | ✔ | [String] | Hex encoded address. | +| **bech32_hrp** | ✔ | [Option] | Optional bech32 hrp. | + +### Return + +Parsed [String]. + ## `parse_bech32_address()` Returns a valid Address parsed from a String.