Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -179,22 +179,22 @@ path = "keetanetwork-ledger"
default-features = false

[workspace.dependencies.keetanetwork-client]
version = "0.4.0"
version = "0.4.1"
path = "keetanetwork-client"
default-features = false

[workspace.dependencies.keetanetwork-bindings]
version = "0.4.1"
version = "0.4.2"
path = "keetanetwork-bindings"
default-features = false

[workspace.dependencies.keetanetwork-client-wasi]
version = "0.4.1"
version = "0.5.0"
path = "keetanetwork-client-wasi"
default-features = false

[workspace.dependencies.keetanetwork-client-wasm]
version = "0.4.1"
version = "0.4.2"
path = "keetanetwork-client-wasm"
default-features = false

Expand Down
2 changes: 1 addition & 1 deletion keetanetwork-bindings/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "keetanetwork-bindings"
version = "0.4.1"
version = "0.4.2"
edition.workspace = true
authors.workspace = true
license.workspace = true
Expand Down
78 changes: 65 additions & 13 deletions keetanetwork-bindings/src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use alloc::sync::Arc;
use alloc::vec::Vec;
use core::str::FromStr;

use hex::FromHex;
use keetanetwork_account::account::{AccountSigner, AccountVerifier};
use keetanetwork_account::{
Account, AccountPublicKey, Accountable, GenericAccount, KeyECDSASECP256K1, KeyECDSASECP256R1, KeyED25519,
Expand Down Expand Up @@ -97,15 +98,37 @@ pub fn account_from_public_key(key: &str, algorithm: &str) -> Result<AccountRef,
keyable_account(Keyable::PublicKey(bytes), algorithm)
}

/// Build a read-only account from its textual `address`.
pub fn account_from_address(address: &str) -> Result<AccountRef, CodedError> {
let account =
GenericAccount::from_str(address).map_err(|_| CodedError::new("INVALID_ADDRESS", "invalid account address"))?;
/// Build a read-only account from its textual `keeta_…` public-key string.
pub fn account_from_public_key_string(public_key_string: &str) -> Result<AccountRef, CodedError> {
let account = GenericAccount::from_str(public_key_string)
.map_err(|_| CodedError::new("INVALID_PUBLIC_KEY_STRING", "invalid account public-key string"))?;
Ok(Arc::new(account))
}

/// The textual account address.
pub fn account_address(account: &AccountRef) -> String {
/// Build a read-only account from the reference `publicKeyAndType` hex
/// layout: `[key_type_byte || raw_public_key]`, optionally `0x`-prefixed.
/// Accepts signing and identifier key types alike, matching the reference
/// `Account.fromPublicKeyAndType`.
pub fn account_from_public_key_and_type(key_and_type: impl AsRef<str>) -> Result<AccountRef, CodedError> {
let hex_data = key_and_type.as_ref();
let hex_data = hex_data
.strip_prefix("0x")
.or_else(|| hex_data.strip_prefix("0X"))
.unwrap_or(hex_data);

let account = GenericAccount::from_hex(hex_data)
.map_err(|_| CodedError::new("INVALID_PUBLIC_KEY_AND_TYPE", "invalid type-prefixed public key hex"))?;
Ok(Arc::new(account))
}

/// The `0x`-prefixed uppercase hex `[key_type_byte || raw_public_key]`
/// string, matching the reference `publicKeyAndTypeString` getter.
pub fn account_public_key_and_type_string(account: &AccountRef) -> String {
format!("0x{}", hex::encode_upper(account.to_public_key_with_type()))
}

/// The account's textual `keeta_…` public-key string.
pub fn account_public_key_string(account: &AccountRef) -> String {
account.to_string()
}

Expand Down Expand Up @@ -172,15 +195,38 @@ mod tests {
}

#[test]
fn account_round_trips_through_seed_and_address() {
fn account_round_trips_through_seed_and_public_key_string() {
let seed = generate_seed().expect("seed generation must succeed");
let account = account_from_seed(&seed, 0, DEFAULT_ALGORITHM).expect("account derivation must succeed");
let address = account_address(&account);
let reopened = account_from_address(&address).expect("address must parse");
assert_eq!(account_address(&reopened), address);
let public_key_string = account_public_key_string(&account);
let reopened = account_from_public_key_string(&public_key_string).expect("public-key string must parse");
assert_eq!(account_public_key_string(&reopened), public_key_string);
assert_eq!(account_algorithm(&account), DEFAULT_ALGORITHM);
}

#[test]
fn account_round_trips_through_public_key_and_type() {
let seed = generate_seed().expect("seed generation must succeed");
let account = account_from_seed(&seed, 0, DEFAULT_ALGORITHM).expect("account derivation must succeed");

let key_and_type = account_public_key_and_type_string(&account);
assert!(key_and_type.starts_with("0x00"));

let reopened = account_from_public_key_and_type(&key_and_type).expect("type-prefixed hex must parse");
assert_eq!(account_public_key_string(&reopened), account_public_key_string(&account));
assert_eq!(account_public_key_and_type_string(&reopened), key_and_type);
}

#[test]
fn public_key_and_type_accepts_unprefixed_hex() {
let seed = generate_seed().expect("seed generation must succeed");
let account = account_from_seed(&seed, 0, "ed25519").expect("account derivation must succeed");
let unprefixed = hex::encode(account.to_public_key_with_type());

let reopened = account_from_public_key_and_type(unprefixed).expect("unprefixed hex must parse");
assert_eq!(account_public_key_string(&reopened), account_public_key_string(&account));
}

#[test]
fn signatures_verify_against_the_signing_account() {
let seed = generate_seed().expect("seed generation must succeed");
Expand Down Expand Up @@ -234,10 +280,16 @@ mod tests {
"INVALID_PUBLIC_KEY"
);
assert_eq!(
account_from_address("not-an-address")
.expect_err("bad address must fail")
account_from_public_key_string("not-a-public-key-string")
.expect_err("bad public-key string must fail")
.code,
"INVALID_PUBLIC_KEY_STRING"
);
assert_eq!(
account_from_public_key_and_type("0xzz")
.expect_err("bad type-prefixed hex must fail")
.code,
"INVALID_ADDRESS"
"INVALID_PUBLIC_KEY_AND_TYPE"
);
}
}
2 changes: 1 addition & 1 deletion keetanetwork-client-wasi/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "keetanetwork-client-wasi"
version = "0.4.1"
version = "0.5.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ int handle() {
return handle;
}

/** The textual account address. */
public String address() {
return net.takeString(net.handle("keeta_account_address", handle()));
/** The account's textual {@code keeta_…} public-key string. */
public String publicKeyString() {
return net.takeString(net.handle("keeta_account_public_key_string", handle()));
}

/** The signing algorithm name, or {@code "other"} for identifier accounts. */
Expand All @@ -38,6 +38,14 @@ public String publicKey() {
return net.takeString(net.handle("keeta_account_public_key", handle()));
}

/**
* The {@code 0x}-prefixed uppercase {@code [key_type_byte || raw_public_key]}
* hex string, matching the reference {@code publicKeyAndTypeString} getter.
*/
public String publicKeyAndTypeString() {
return net.takeString(net.handle("keeta_account_public_key_and_type_string", handle()));
}

/** Produce a detached signature over {@code message}. */
public byte[] sign(byte[] message) {
int messagePtr = net.write(message);
Expand Down Expand Up @@ -93,7 +101,7 @@ public Account generateMultisigIdentifier(byte[] previousHash, int operationInde

@Override
public String toString() {
return address();
return publicKeyString();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ public Account account(String seedHex, int index, Algorithm algorithm) {
return new Account(net, handle);
}

/**
* Derive an account from a hex seed at a derivation index under the
* default algorithm ({@code ecdsa_secp256k1}), matching the reference
* {@code Account.fromSeed(seed, index)} overload.
*/
public Account account(String seedHex, int index) {
return account(seedHex, index, Algorithm.ECDSA_SECP256K1);
}

/** Build an account from a hex-encoded private key. */
public Account accountFromPrivateKey(String privateKeyHex, Algorithm algorithm) {
byte[] key = privateKeyHex.getBytes(StandardCharsets.UTF_8);
Expand Down Expand Up @@ -89,11 +98,22 @@ public Account accountFromPublicKey(String publicKeyHex, Algorithm algorithm) {
return new Account(net, handle);
}

/** Build a read-only account from its textual address. */
public Account address(String address) {
byte[] bytes = address.getBytes(StandardCharsets.UTF_8);
/** Build a read-only account from its textual {@code keeta_…} public-key string. */
public Account accountFromPublicKeyString(String publicKeyString) {
byte[] bytes = publicKeyString.getBytes(StandardCharsets.UTF_8);
int ptr = net.write(bytes);
return new Account(net, net.handle("keeta_account_from_public_key_string", ptr, bytes.length));
}

/**
* Build a read-only account from the {@code [key_type_byte || raw_public_key]}
* hex layout (optionally {@code 0x}-prefixed), matching the reference
* {@code Account.fromPublicKeyAndType}.
*/
public Account accountFromPublicKeyAndType(String keyAndTypeHex) {
byte[] bytes = keyAndTypeHex.getBytes(StandardCharsets.UTF_8);
int ptr = net.write(bytes);
return new Account(net, net.handle("keeta_account_from_address", ptr, bytes.length));
return new Account(net, net.handle("keeta_account_from_public_key_and_type", ptr, bytes.length));
}

/* ---------------------------- certificates -------------------------- */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import network.keeta.node.api.VoteApi;
import network.keeta.node.invoker.ApiClient;
import network.keeta.node.invoker.ApiException;
import network.keeta.node.model.CreateVote200Response;
import network.keeta.node.model.CreateVoteResponse;
import network.keeta.node.model.CreateVoteRequest;
import network.keeta.node.model.PublishVoteStapleRequest;
import network.keeta.node.model.Vote;
Expand Down Expand Up @@ -50,18 +50,18 @@ public String nodeVersion() {

/** The {@code account} balance of {@code token} as a 0x-prefixed hexadecimal string. */
public String balance(Account account, Account token) {
return attempt(() -> ledgerApi.getAccountBalance(account.address(), token.address()).getBalance(), "balance");
return attempt(() -> ledgerApi.getAccountBalance(account.publicKeyString(), token.publicKeyString()).getBalance(), "balance");
}

/** The account's current head block hash (hex), or {@code null} for an unopened account. */
public String headHash(Account account) {
return attempt(() -> ledgerApi.getAccountState(account.address()).getCurrentHeadBlock(), "account state");
return attempt(() -> ledgerApi.getAccountState(account.publicKeyString()).getCurrentHeadBlock(), "account state");
}

/** Total supply of {@code token} as a 0x-prefixed hex string (token accounts only); {@code null} when absent. */
public String supply(Account token) {
return attempt(() -> {
var info = ledgerApi.getAccountState(token.address()).getInfo();
var info = ledgerApi.getAccountState(token.publicKeyString()).getInfo();
return info == null ? null : info.getSupply();
}, "token supply");
}
Expand Down Expand Up @@ -188,11 +188,11 @@ private Block.SignedBlock buildFeeBlock(Account feeSigner, Account baseToken, Li

/** The fee block's previous: {@code feeSigner}'s last block in {@code blocks}, else its ledger head. */
private String feeBlockPrevious(Account feeSigner, List<Block.SignedBlock> blocks) {
String signerAddress = feeSigner.address();
String signerAddress = feeSigner.publicKeyString();
String previous = null;
for (Block.SignedBlock block : blocks) {
try (Account account = block.account()) {
if (account.address().equals(signerAddress)) {
if (account.publicKeyString().equals(signerAddress)) {
previous = block.hashHex();
}
}
Expand Down Expand Up @@ -274,7 +274,7 @@ private String requestVote(List<String> blocksBase64, String priorVoteBase64) {
CreateVoteRequest request = new CreateVoteRequest()
.blocks(blocksBase64)
.votes(priorVoteBase64 == null ? null : List.of(priorVoteBase64));
CreateVote200Response response = attempt(() -> voteApi.createVote(request), "vote");
CreateVoteResponse response = attempt(() -> voteApi.createVote(request), "vote");
Vote vote = response.getVote();

if (vote == null || vote.get$Binary() == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public static void main(String[] args) throws Exception {
address, publicKeyHex, message, referenceSignature, plaintext, referenceCiphertext, result);
roundTripPublicKey(keeta, algorithm, name, rawPublicKeyHex, address, publicKeyHex, message,
referenceSignature, plaintext, result);
roundTripPublicKeyAndType(keeta, name, publicKeyHex, address);
checkPassphrase(keeta, algorithm, name, words, index, passphraseAddress);
}
}
Expand All @@ -71,7 +72,7 @@ private static void roundTripPrivateKey(Keeta keeta, Algorithm algorithm, String
String address, String publicKeyHex, byte[] message, byte[] referenceSignature, byte[] plaintext,
byte[] referenceCiphertext, ObjectNode result) {
try (Account account = keeta.accountFromPrivateKey(privateKeyHex, algorithm)) {
check(account.address().equals(address), name + " private-key address must match the reference");
check(account.publicKeyString().equals(address), name + " private-key address must match the reference");
check(account.publicKey().equalsIgnoreCase(publicKeyHex), name + " private-key public key must match");
check(account.verify(message, referenceSignature), name + " must verify the reference signature");
check(Arrays.equals(account.decrypt(referenceCiphertext), plaintext),
Expand All @@ -87,7 +88,7 @@ private static void roundTripPublicKey(Keeta keeta, Algorithm algorithm, String
String address, String publicKeyHex, byte[] message, byte[] referenceSignature, byte[] plaintext,
ObjectNode result) {
try (Account account = keeta.accountFromPublicKey(rawPublicKeyHex, algorithm)) {
check(account.address().equals(address), name + " public-key address must match the reference");
check(account.publicKeyString().equals(address), name + " public-key address must match the reference");
check(account.publicKey().equalsIgnoreCase(publicKeyHex),
name + " read-only public key must match the reference");
check(account.verify(message, referenceSignature),
Expand All @@ -97,11 +98,21 @@ private static void roundTripPublicKey(Keeta keeta, Algorithm algorithm, String
}
}

/** Reconstruct from the type-prefixed key hex; assert address and getter round-trip. */
private static void roundTripPublicKeyAndType(Keeta keeta, String name, String publicKeyHex, String address) {
try (Account account = keeta.accountFromPublicKeyAndType(publicKeyHex)) {
check(account.publicKeyString().equals(address),
name + " public-key-and-type address must match the reference");
check(account.publicKeyAndTypeString().equals("0x" + publicKeyHex.toUpperCase()),
name + " publicKeyAndTypeString must round-trip the reference key material");
}
}

/** Reconstruct from the passphrase; assert the derived address matches the reference. */
private static void checkPassphrase(Keeta keeta, Algorithm algorithm, String name, List<String> words,
int index, String passphraseAddress) {
try (Account account = keeta.accountFromPassphrase(words, index, algorithm)) {
check(account.address().equals(passphraseAddress),
check(account.publicKeyString().equals(passphraseAddress),
name + " passphrase address must match the reference");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public static void main(String[] args) {

try (Account trusted = keeta.account(trustedSeed, 0, Algorithm.ED25519);
Account recipient = keeta.account(trustedSeed, RECIPIENT_INDEX, Algorithm.ED25519);
Account base = keeta.address(baseToken)) {
Account base = keeta.accountFromPublicKeyString(baseToken)) {
transferRoundTrip(client, trusted, recipient, base);
certificateRoundTrip(keeta, client, trusted, certificateDerHex);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public static void main(String[] args) {

try (Account trusted = keeta.account(trustedSeed, 0, Algorithm.ED25519);
Account recipient = keeta.account(trustedSeed, 7, Algorithm.ED25519);
Account base = keeta.address(baseTokenAddress)) {
Account base = keeta.accountFromPublicKeyString(baseTokenAddress)) {
String head = client.headHash(trusted);
check(head != null && !head.isBlank(), "funded account must have a head block");

Expand Down
Loading
Loading