diff --git a/Cargo.lock b/Cargo.lock index 2b6697a..861025f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1268,7 +1268,7 @@ dependencies = [ [[package]] name = "keetanetwork-bindings" -version = "0.4.1" +version = "0.4.2" dependencies = [ "chrono", "hex", @@ -1304,7 +1304,7 @@ dependencies = [ [[package]] name = "keetanetwork-client" -version = "0.4.0" +version = "0.4.1" dependencies = [ "async-trait", "base64", @@ -1342,7 +1342,7 @@ dependencies = [ [[package]] name = "keetanetwork-client-wasi" -version = "0.4.1" +version = "0.5.0" dependencies = [ "hex", "keetanetwork-account", @@ -1359,7 +1359,7 @@ dependencies = [ [[package]] name = "keetanetwork-client-wasm" -version = "0.4.1" +version = "0.4.2" dependencies = [ "chrono", "hex", diff --git a/Cargo.toml b/Cargo.toml index d78a4ab..f8fadc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/keetanetwork-bindings/Cargo.toml b/keetanetwork-bindings/Cargo.toml index 8e37099..c0bab51 100644 --- a/keetanetwork-bindings/Cargo.toml +++ b/keetanetwork-bindings/Cargo.toml @@ -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 diff --git a/keetanetwork-bindings/src/account.rs b/keetanetwork-bindings/src/account.rs index 6222498..e4bf782 100644 --- a/keetanetwork-bindings/src/account.rs +++ b/keetanetwork-bindings/src/account.rs @@ -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, @@ -97,15 +98,37 @@ pub fn account_from_public_key(key: &str, algorithm: &str) -> Result Result { - 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 { + 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) -> Result { + 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() } @@ -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"); @@ -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" ); } } diff --git a/keetanetwork-client-wasi/Cargo.toml b/keetanetwork-client-wasi/Cargo.toml index 3458f50..4206b15 100644 --- a/keetanetwork-client-wasi/Cargo.toml +++ b/keetanetwork-client-wasi/Cargo.toml @@ -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 diff --git a/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/Account.java b/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/Account.java index 849ac62..28fcbe0 100644 --- a/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/Account.java +++ b/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/Account.java @@ -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. */ @@ -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); @@ -93,7 +101,7 @@ public Account generateMultisigIdentifier(byte[] previousHash, int operationInde @Override public String toString() { - return address(); + return publicKeyString(); } @Override diff --git a/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/Keeta.java b/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/Keeta.java index 08f57e1..da96d16 100644 --- a/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/Keeta.java +++ b/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/Keeta.java @@ -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); @@ -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 -------------------------- */ diff --git a/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/UserClient.java b/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/UserClient.java index b9d0f39..bba3e5f 100644 --- a/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/UserClient.java +++ b/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/UserClient.java @@ -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; @@ -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"); } @@ -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 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(); } } @@ -274,7 +274,7 @@ private String requestVote(List 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) { diff --git a/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/AccountParity.java b/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/AccountParity.java index a9be00f..5d59d17 100644 --- a/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/AccountParity.java +++ b/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/AccountParity.java @@ -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); } } @@ -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), @@ -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), @@ -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 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"); } } diff --git a/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/ClientOperations.java b/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/ClientOperations.java index 95c2c9c..8d3778a 100644 --- a/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/ClientOperations.java +++ b/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/ClientOperations.java @@ -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); } diff --git a/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/FeeTransfer.java b/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/FeeTransfer.java index 8017909..7c5ba09 100644 --- a/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/FeeTransfer.java +++ b/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/FeeTransfer.java @@ -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"); diff --git a/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/MultisigSigner.java b/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/MultisigSigner.java index 85335c9..5f5a00a 100644 --- a/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/MultisigSigner.java +++ b/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/MultisigSigner.java @@ -35,25 +35,25 @@ public static void main(String[] args) { System.out.println("[harness] node version " + version.trim()); try (Account trusted = keeta.account(trustedSeed, 0, Algorithm.ED25519); - Account base = keeta.address(baseToken); + Account base = keeta.accountFromPublicKeyString(baseToken); Account signer1 = keeta.account(trustedSeed, 1, Algorithm.ED25519); Account signer2 = keeta.account(trustedSeed, 2, Algorithm.ED25519); Account signer3 = keeta.account(trustedSeed, 3, Algorithm.ED25519)) { - System.out.println("[harness] funded account " + trusted.address()); + System.out.println("[harness] funded account " + trusted.publicKeyString()); System.out.println("[harness] base balance " + client.balance(trusted, base).trim()); String openingHead = client.headHash(trusted); check(openingHead != null && !openingHead.isBlank(), "funded account must have a head block"); try (Account multisig = trusted.generateMultisigIdentifier(hexDecode(openingHead), 0)) { - System.out.println("[harness] multisig account " + multisig.address()); + System.out.println("[harness] multisig account " + multisig.publicKeyString()); String afterMultisig = createMultisig(keeta, client, trusted, multisig, List.of(signer1, signer2, signer3), network, openingHead); check(client.headHash(trusted).equalsIgnoreCase(afterMultisig), "create-multisig block must be the funded head"); try (Account customToken = trusted.generateIdentifier(IdentifierType.TOKEN, hexDecode(afterMultisig), 0)) { - System.out.println("[harness] custom token " + customToken.address()); + System.out.println("[harness] custom token " + customToken.publicKeyString()); String afterToken = createToken(keeta, client, trusted, customToken, network, afterMultisig); check(client.headHash(trusted).equalsIgnoreCase(afterToken), "create-token block must be the funded head"); diff --git a/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/TokenSupply.java b/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/TokenSupply.java index 7854900..349775a 100644 --- a/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/TokenSupply.java +++ b/keetanetwork-client-wasi/bindings/java/src/main/java/network/keeta/wasi/harness/TokenSupply.java @@ -42,7 +42,7 @@ public static void main(String[] args) { check(trustedHead != null && !trustedHead.isBlank(), "funded account must have a head block"); try (Account token = trusted.generateIdentifier(IdentifierType.TOKEN, hexDecode(trustedHead), 0)) { - System.out.println("[harness] minting token " + token.address()); + System.out.println("[harness] minting token " + token.publicKeyString()); mint(keeta, client, trusted, token, holder, network, trustedHead); diff --git a/keetanetwork-client-wasi/host-tests/tests/p2_net.rs b/keetanetwork-client-wasi/host-tests/tests/p2_net.rs index a66d29e..287cf7b 100644 --- a/keetanetwork-client-wasi/host-tests/tests/p2_net.rs +++ b/keetanetwork-client-wasi/host-tests/tests/p2_net.rs @@ -109,26 +109,63 @@ async fn account_from_seed( bindings .keeta_client_crypto() .account() - .call_from_seed(&mut *store, seed, index, KeyAlgorithm::Ed25519) + .call_from_seed(&mut *store, seed, index, Some(KeyAlgorithm::Ed25519)) .await? .map_err(coded) } -/// Parse a textual `keeta_…` address into an `account` resource via the -/// exported `crypto` interface, returning its host handle. -async fn account_from_address( +/// Parse a textual `keeta_…` public-key string into an `account` resource via +/// the exported `crypto` interface, returning its host handle. +async fn account_from_public_key_string( store: &mut Store, bindings: &KeetaClient, - address: &str, + public_key_string: &str, ) -> wasmtime::Result { bindings .keeta_client_crypto() .account() - .call_from_address(&mut *store, address) + .call_from_public_key_string(&mut *store, public_key_string) .await? .map_err(coded) } +#[tokio::test] +async fn p2_account_round_trips_through_public_key_and_type() -> wasmtime::Result<()> { + let (mut store, bindings) = instantiate().await?; + let account_test = bindings.keeta_client_crypto().account(); + + let account = account_from_seed(&mut store, &bindings, &trusted_seed(), 0).await?; + let address = account_test + .call_public_key_string(&mut store, account) + .await?; + let key_and_type = account_test + .call_public_key_and_type_string(&mut store, account) + .await?; + assert!(key_and_type.starts_with("0x"), "the getter must be 0x-prefixed hex"); + + let reopened = account_test + .call_from_public_key_and_type(&mut store, &key_and_type) + .await? + .map_err(coded)?; + let reopened_address = account_test + .call_public_key_string(&mut store, reopened) + .await?; + assert_eq!(reopened_address, address, "the reopened account must keep its address"); + + // An omitted algorithm must select the default (`ecdsa-secp256k1`, + // key type byte 0x00), matching the browser binding and the reference. + let defaulted = account_test + .call_from_seed(&mut store, &trusted_seed(), 0, None) + .await? + .map_err(coded)?; + let defaulted_key = account_test + .call_public_key_and_type_string(&mut store, defaulted) + .await?; + assert!(defaulted_key.starts_with("0x00"), "the default algorithm must be ecdsa-secp256k1"); + + Ok(()) +} + #[tokio::test] async fn p2_reads_against_e2e_node() -> wasmtime::Result<()> { let mut harness = E2eNode::start()?; @@ -147,8 +184,8 @@ async fn p2_reads_against_e2e_node() -> wasmtime::Result<()> { let node = bindings.keeta_client_node(); // Typed account resources for the harness's textual addresses. - let trusted_account = account_from_address(&mut store, &bindings, &trusted).await?; - let base_token_account = account_from_address(&mut store, &bindings, &base_token).await?; + let trusted_account = account_from_public_key_string(&mut store, &bindings, &trusted).await?; + let base_token_account = account_from_public_key_string(&mut store, &bindings, &base_token).await?; // `node` resource: anonymous client bound to the rep URL. let client = node.client().call_constructor(&mut store, &api).await?; @@ -567,8 +604,8 @@ async fn p2_writes_against_e2e_node() -> wasmtime::Result<()> { let node = bindings.keeta_client_node(); // Typed account resources for the harness's textual addresses. - let representative_account = account_from_address(&mut store, &bindings, &representative).await?; - let base_token_account = account_from_address(&mut store, &bindings, &base_token).await?; + let representative_account = account_from_public_key_string(&mut store, &bindings, &representative).await?; + let base_token_account = account_from_public_key_string(&mut store, &bindings, &base_token).await?; // Bind a signing client to the trusted (genesis) seed; it is its own // operating account. @@ -952,11 +989,11 @@ async fn p2_multisig_signer_against_e2e_node() -> wasmtime::Result<()> { .call_address(&mut store, user) .await? .map_err(coded)?; - let user_account = account_from_address(&mut store, &bindings, &user_address).await?; + let user_account = account_from_public_key_string(&mut store, &bindings, &user_address).await?; // Fund the user so it has a settled balance and a head to chain onto. let base_token = ready_field(&harness, "baseToken"); - let base_token_account = account_from_address(&mut store, &bindings, &base_token).await?; + let base_token_account = account_from_public_key_string(&mut store, &bindings, &base_token).await?; let trusted_account = account_from_seed(&mut store, &bindings, &trusted_seed(), 0).await?; let funder = node .user_client() @@ -982,7 +1019,7 @@ async fn p2_multisig_signer_against_e2e_node() -> wasmtime::Result<()> { .await? .map_err(coded)?; assert!(!multisig.is_empty(), "the multisig identifier address must derive"); - let multisig_account = account_from_address(&mut store, &bindings, &multisig).await?; + let multisig_account = account_from_public_key_string(&mut store, &bindings, &multisig).await?; // User block: create the 2-of-3 multisig and grant it ADMIN over the user. let user_head = node @@ -1051,7 +1088,7 @@ async fn p2_multisig_signer_against_e2e_node() -> wasmtime::Result<()> { .await? .map_err(coded)?; assert!(!custom_token.is_empty(), "the custom token address must be returned"); - let custom_token_account = account_from_address(&mut store, &bindings, &custom_token).await?; + let custom_token_account = account_from_public_key_string(&mut store, &bindings, &custom_token).await?; // Grant the multisig ADMIN on the token's own chain (the entity whose ACL // changes), signed by the trusted creator. diff --git a/keetanetwork-client-wasi/host-tests/tests/smoke.rs b/keetanetwork-client-wasi/host-tests/tests/smoke.rs index 8786932..f9f5ecd 100644 --- a/keetanetwork-client-wasi/host-tests/tests/smoke.rs +++ b/keetanetwork-client-wasi/host-tests/tests/smoke.rs @@ -35,7 +35,9 @@ struct Abi { last_error_code: TypedFunc<(), i32>, generate_seed: TypedFunc<(), i32>, account_from_seed: TypedFunc<(i32, i32, i32, i32, i32), i32>, - account_address: TypedFunc, + account_from_public_key_and_type: TypedFunc<(i32, i32), i32>, + account_public_key_string: TypedFunc, + account_public_key_and_type_string: TypedFunc, account_sign: TypedFunc<(i32, i32, i32), i32>, account_verify: TypedFunc<(i32, i32, i32, i32, i32), i32>, account_encrypt: TypedFunc<(i32, i32, i32), i32>, @@ -73,7 +75,11 @@ impl Abi { last_error_code: instance.get_typed_func(&mut *store, "keeta_last_error_code")?, generate_seed: instance.get_typed_func(&mut *store, "keeta_generate_seed")?, account_from_seed: instance.get_typed_func(&mut *store, "keeta_account_from_seed")?, - account_address: instance.get_typed_func(&mut *store, "keeta_account_address")?, + account_from_public_key_and_type: instance + .get_typed_func(&mut *store, "keeta_account_from_public_key_and_type")?, + account_public_key_string: instance.get_typed_func(&mut *store, "keeta_account_public_key_string")?, + account_public_key_and_type_string: instance + .get_typed_func(&mut *store, "keeta_account_public_key_and_type_string")?, account_sign: instance.get_typed_func(&mut *store, "keeta_account_sign")?, account_verify: instance.get_typed_func(&mut *store, "keeta_account_verify")?, account_encrypt: instance.get_typed_func(&mut *store, "keeta_account_encrypt")?, @@ -216,9 +222,9 @@ fn p1_derives_account_and_signs_an_opening_block() -> wasmtime::Result<()> { let user = abi.account_from_seed(&mut store, &seed, 0)?; let rep = abi.account_from_seed(&mut store, &seed, 1)?; - let address_handle = abi.account_address.call(&mut store, user)?; - let address = abi.take_string(&mut store, address_handle)?; - assert!(!address.is_empty(), "the account must have an address"); + let string_handle = abi.account_public_key_string.call(&mut store, user)?; + let public_key_string = abi.take_string(&mut store, string_handle)?; + assert!(!public_key_string.is_empty(), "the account must have a public-key string"); let operation = abi.checked(&mut store, &abi.op_set_rep, rep)?; @@ -279,6 +285,40 @@ fn p1_account_signs_verifies_and_encrypts_round_trip() -> wasmtime::Result<()> { Ok(()) } +#[test] +fn p1_account_round_trips_through_public_key_and_type() -> wasmtime::Result<()> { + let (mut store, abi) = instantiate()?; + + let account = abi.seeded_account(&mut store, 0)?; + let string_handle = abi.account_public_key_string.call(&mut store, account)?; + let address = abi.take_string(&mut store, string_handle)?; + + let key_handle = abi + .account_public_key_and_type_string + .call(&mut store, account)?; + let key_and_type = abi.take_string(&mut store, key_handle)?; + assert!(key_and_type.starts_with("0x"), "the getter must be 0x-prefixed hex"); + + let (hex_ptr, hex_len) = abi.write(&mut store, key_and_type.as_bytes())?; + let reopened = abi.checked(&mut store, &abi.account_from_public_key_and_type, (hex_ptr, hex_len))?; + let string_handle = abi.account_public_key_string.call(&mut store, reopened)?; + let reopened_address = abi.take_string(&mut store, string_handle)?; + assert_eq!(reopened_address, address, "the reopened account must keep its address"); + + // An empty algorithm must select the default (`ecdsa_secp256k1`, + // key type byte 0x00), matching the browser binding and the reference. + let seed = abi.generate_seed_string(&mut store)?; + let (seed_ptr, seed_len) = abi.write(&mut store, seed.as_bytes())?; + let defaulted = abi.checked(&mut store, &abi.account_from_seed, (seed_ptr, seed_len, 0, 0, 0))?; + let key_handle = abi + .account_public_key_and_type_string + .call(&mut store, defaulted)?; + let defaulted_key = abi.take_string(&mut store, key_handle)?; + assert!(defaulted_key.starts_with("0x00"), "the default algorithm must be ecdsa_secp256k1"); + + Ok(()) +} + #[test] fn p1_parses_a_certificate_and_round_trips_pem_der_and_validity() -> wasmtime::Result<()> { use std::time::{SystemTime, UNIX_EPOCH}; diff --git a/keetanetwork-client-wasi/src/p1/mod.rs b/keetanetwork-client-wasi/src/p1/mod.rs index d14bb5c..7dd6376 100644 --- a/keetanetwork-client-wasi/src/p1/mod.rs +++ b/keetanetwork-client-wasi/src/p1/mod.rs @@ -395,7 +395,13 @@ pub unsafe extern "C" fn keeta_account_from_seed( return 0; }; - match pure::account_from_seed(&seed, index as u32, &algorithm) { + // An empty algorithm selects the default, matching the browser binding. + let algorithm = if algorithm.is_empty() { + pure::DEFAULT_ALGORITHM + } else { + &algorithm + }; + match pure::account_from_seed(&seed, index as u32, algorithm) { Ok(account) => store_account(account), Err(error) => fail(error), } @@ -458,23 +464,42 @@ pub unsafe extern "C" fn keeta_account_from_public_key( } } -/// Build a read-only account from its textual address; returns an account -/// handle. +/// Build a read-only account from its textual `keeta_…` public-key string; +/// returns an account handle. #[no_mangle] -pub unsafe extern "C" fn keeta_account_from_address(address_ptr: i32, address_len: i32) -> i32 { - let Some(address) = string_in(address_ptr, address_len) else { +pub unsafe extern "C" fn keeta_account_from_public_key_string(string_ptr: i32, string_len: i32) -> i32 { + let Some(public_key_string) = string_in(string_ptr, string_len) else { return 0; }; - match pure::account_from_address(&address) { + match pure::account_from_public_key_string(&public_key_string) { Ok(account) => store_account(account), Err(error) => fail(error), } } -/// The account address as a bytes handle. +/// Build a read-only account from the `[key_type_byte || raw_public_key]` +/// hex layout (optionally `0x`-prefixed); returns an account handle. +#[no_mangle] +pub unsafe extern "C" fn keeta_account_from_public_key_and_type(hex_ptr: i32, hex_len: i32) -> i32 { + let Some(key_and_type) = string_in(hex_ptr, hex_len) else { + return 0; + }; + match pure::account_from_public_key_and_type(&key_and_type) { + Ok(account) => store_account(account), + Err(error) => fail(error), + } +} + +/// The `0x`-prefixed uppercase type-prefixed public key hex as a bytes handle. +#[no_mangle] +pub extern "C" fn keeta_account_public_key_and_type_string(handle: i32) -> i32 { + account(handle).map_or(0, |account| store_bytes(pure::account_public_key_and_type_string(&account).into_bytes())) +} + +/// The account's textual `keeta_…` public-key string as a bytes handle. #[no_mangle] -pub extern "C" fn keeta_account_address(handle: i32) -> i32 { - account(handle).map_or(0, |account| store_bytes(pure::account_address(&account).into_bytes())) +pub extern "C" fn keeta_account_public_key_string(handle: i32) -> i32 { + account(handle).map_or(0, |account| store_bytes(pure::account_public_key_string(&account).into_bytes())) } /// The account algorithm name as a bytes handle. diff --git a/keetanetwork-client-wasi/src/p2/mod.rs b/keetanetwork-client-wasi/src/p2/mod.rs index a1ddbf1..85fa3d0 100644 --- a/keetanetwork-client-wasi/src/p2/mod.rs +++ b/keetanetwork-client-wasi/src/p2/mod.rs @@ -110,7 +110,7 @@ impl Guest for Component { let account = &signer.get::().account; let previous = previous.map(|hash| decode_hash(&hash)).transpose()?; let identifier = pure::generate_identifier(account, kind.into(), previous, op_index)?; - Ok(pure::account_address(&identifier)) + Ok(pure::account_public_key_string(&identifier)) } } @@ -137,8 +137,9 @@ struct AccountResource { } impl GuestAccount for AccountResource { - fn from_seed(seed: String, index: u32, algorithm: WitKeyAlgorithm) -> Result { - let account = pure::account_from_seed(&seed, index, algorithm_name(algorithm))?; + fn from_seed(seed: String, index: u32, algorithm: Option) -> Result { + let algorithm = algorithm.map_or(pure::DEFAULT_ALGORITHM, algorithm_name); + let account = pure::account_from_seed(&seed, index, algorithm)?; Ok(WitAccount::new(Self { account })) } @@ -157,8 +158,13 @@ impl GuestAccount for AccountResource { Ok(WitAccount::new(Self { account })) } - fn from_address(address: String) -> Result { - let account = pure::account_from_address(&address)?; + fn from_public_key_string(public_key_string: String) -> Result { + let account = pure::account_from_public_key_string(&public_key_string)?; + Ok(WitAccount::new(Self { account })) + } + + fn from_public_key_and_type(key_and_type: String) -> Result { + let account = pure::account_from_public_key_and_type(&key_and_type)?; Ok(WitAccount::new(Self { account })) } @@ -170,8 +176,8 @@ impl GuestAccount for AccountResource { pure::generate_passphrase().unwrap_or_default() } - fn address(&self) -> String { - pure::account_address(&self.account) + fn public_key_string(&self) -> String { + pure::account_public_key_string(&self.account) } fn kind(&self) -> WitAccountKind { @@ -190,6 +196,10 @@ impl GuestAccount for AccountResource { pure::account_public_key(&self.account) } + fn public_key_and_type_string(&self) -> String { + pure::account_public_key_and_type_string(&self.account) + } + fn sign(&self, message: Vec) -> Result, CodedError> { Ok(pure::account_sign(&self.account, &message)?) } @@ -471,7 +481,7 @@ impl TryFrom for SwapTokenAmount { fn try_from(leg: WitSwapTokenAmount) -> Result { let token = leg .token - .map(|token| pure::account_from_address(&token)) + .map(|token| pure::account_from_public_key_string(&token)) .transpose()?; let amount = leg.amount.map(|amount| parse_amount(&amount)).transpose()?; @@ -780,7 +790,7 @@ impl GuestUserClient for AccountClient { } fn address(&self) -> Result { - Ok(pure::account_address(&self.inner.account()?)) + Ok(pure::account_public_key_string(&self.inner.account()?)) } fn balance(&self, token: AccountBorrow<'_>) -> Result { @@ -988,7 +998,7 @@ impl GuestUserClient for AccountClient { let identifier = run(self .inner .generate_identifier(KeyPairType::MULTISIG, Some(arguments)))?; - Ok(pure::account_address(&identifier)) + Ok(pure::account_public_key_string(&identifier)) } fn generate_identifier(&self, kind: WitIdentifierKind) -> Result { @@ -1002,7 +1012,7 @@ impl GuestUserClient for AccountClient { } let identifier = run(self.inner.generate_identifier(kind.into(), None))?; - Ok(pure::account_address(&identifier)) + Ok(pure::account_public_key_string(&identifier)) } fn create_swap( diff --git a/keetanetwork-client-wasi/src/pure.rs b/keetanetwork-client-wasi/src/pure.rs index f6dd865..8e7220e 100644 --- a/keetanetwork-client-wasi/src/pure.rs +++ b/keetanetwork-client-wasi/src/pure.rs @@ -21,9 +21,10 @@ use keetanetwork_vote::{ValidationConfig, Vote, VoteQuote, VoteStaple}; /// The account primitive operations live in the shared `keetanetwork-bindings` /// crate so every binding boundary reuses a single definition. pub use keetanetwork_bindings::account::{ - account_address, account_algorithm, account_decrypt, account_encrypt, account_from_address, - account_from_passphrase, account_from_private_key, account_from_public_key, account_from_seed, account_public_key, - account_sign, account_verify, generate_passphrase, generate_seed, DEFAULT_ALGORITHM, + account_algorithm, account_decrypt, account_encrypt, account_from_passphrase, account_from_private_key, + account_from_public_key, account_from_public_key_and_type, account_from_public_key_string, account_from_seed, + account_public_key, account_public_key_and_type_string, account_public_key_string, account_sign, account_verify, + generate_passphrase, generate_seed, DEFAULT_ALGORITHM, }; /// The base X.509 certificate primitive operations also live in the shared diff --git a/keetanetwork-client-wasi/wit/world.wit b/keetanetwork-client-wasi/wit/world.wit index 63062fc..940fde9 100644 --- a/keetanetwork-client-wasi/wit/world.wit +++ b/keetanetwork-client-wasi/wit/world.wit @@ -223,11 +223,12 @@ interface crypto { use types.{coded-error, key-algorithm, account-kind}; /// A Keeta account: a signer derived from a seed, or a read-only account - /// parsed from its textual address. + /// parsed from its textual public-key string. resource account { /// Derive an account from a 32-byte hex `seed` at `index` under - /// `algorithm`. - from-seed: static func(seed: string, index: u32, algorithm: key-algorithm) -> result; + /// `algorithm`; defaults to `ecdsa-secp256k1` when omitted, matching + /// the browser binding and the reference SDK. + from-seed: static func(seed: string, index: u32, algorithm: option) -> result; /// Build an account from a hex-encoded private `key` under `algorithm`. from-private-key: static func(key: string, algorithm: key-algorithm) -> result; /// Derive an account from a BIP39 mnemonic `words` at `index` under @@ -236,19 +237,27 @@ interface crypto { /// Build a read-only account from a hex-encoded public `key` under /// `algorithm`. from-public-key: static func(key: string, algorithm: key-algorithm) -> result; - /// Parse a read-only account from its textual `keeta_…` address. - from-address: static func(address: string) -> result; + /// Parse a read-only account from its textual `keeta_…` public-key + /// string. + from-public-key-string: static func(public-key-string: string) -> result; + /// Build a read-only account from the `[key_type_byte || raw_public_key]` + /// hex layout (optionally `0x`-prefixed), matching the reference + /// `Account.fromPublicKeyAndType`. + from-public-key-and-type: static func(key-and-type: string) -> result; /// A fresh random 32-byte seed, hex-encoded. generate-seed: static func() -> string; /// A fresh random BIP39 mnemonic. generate-passphrase: static func() -> list; - /// The textual `keeta_…` address. - address: func() -> string; + /// The textual `keeta_…` public-key string. + public-key-string: func() -> string; /// What the account is: a signing account under a key algorithm, or a /// derived identifier account. kind: func() -> account-kind; /// The type-prefixed public key, hex-encoded. public-key: func() -> string; + /// The `0x`-prefixed uppercase `[key_type_byte || raw_public_key]` hex + /// string, matching the reference `publicKeyAndTypeString` getter. + public-key-and-type-string: func() -> string; /// Sign `message`, returning the raw signature bytes. sign: func(message: list) -> result, coded-error>; /// Whether `signature` is a valid signature of `message` by this account. diff --git a/keetanetwork-client-wasm/Cargo.toml b/keetanetwork-client-wasm/Cargo.toml index 4946853..94d0347 100644 --- a/keetanetwork-client-wasm/Cargo.toml +++ b/keetanetwork-client-wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keetanetwork-client-wasm" -version = "0.4.1" +version = "0.4.2" edition.workspace = true authors.workspace = true license.workspace = true diff --git a/keetanetwork-client-wasm/src/account.rs b/keetanetwork-client-wasm/src/account.rs index af71edc..f9ed583 100644 --- a/keetanetwork-client-wasm/src/account.rs +++ b/keetanetwork-client-wasm/src/account.rs @@ -71,19 +71,29 @@ impl Account { .map_err(coded) } - /// Build a read-only account from its textual `address`. Suitable as a - /// recipient or token operand, but cannot sign. - #[wasm_bindgen(js_name = fromAddress)] - pub fn from_address(address: String) -> JsResult { - account::account_from_address(&address) + /// Build a read-only account from its textual `keeta_…` public-key string. + /// Suitable as a recipient or token operand, but cannot sign. + #[wasm_bindgen(js_name = fromPublicKeyString)] + pub fn from_public_key_string(public_key_string: String) -> JsResult { + account::account_from_public_key_string(&public_key_string) .map(Account::from) .map_err(coded) } - /// The textual account address. - #[wasm_bindgen(getter)] - pub fn address(&self) -> String { - account::account_address(&self.inner) + /// Build a read-only account from the `[key_type_byte || raw_public_key]` + /// hex layout (optionally `0x`-prefixed), matching the reference + /// `Account.fromPublicKeyAndType`. + #[wasm_bindgen(js_name = fromPublicKeyAndType)] + pub fn from_public_key_and_type(key_and_type: String) -> JsResult { + account::account_from_public_key_and_type(&key_and_type) + .map(Account::from) + .map_err(coded) + } + + /// The account's textual `keeta_…` public-key string. + #[wasm_bindgen(getter, js_name = publicKeyString)] + pub fn public_key_string(&self) -> String { + account::account_public_key_string(&self.inner) } /// The signing algorithm name, or `"other"` for identifier accounts. @@ -98,6 +108,13 @@ impl Account { account::account_public_key(&self.inner) } + /// The `0x`-prefixed uppercase `[key_type_byte || raw_public_key]` hex + /// string, matching the reference `publicKeyAndTypeString` getter. + #[wasm_bindgen(getter, js_name = publicKeyAndTypeString)] + pub fn public_key_and_type_string(&self) -> String { + account::account_public_key_and_type_string(&self.inner) + } + /// Derive an identifier account of `kind` relative to this account. #[wasm_bindgen(js_name = generateIdentifier)] pub fn generate_identifier( diff --git a/keetanetwork-client-wasm/src/lib.rs b/keetanetwork-client-wasm/src/lib.rs index 78e35eb..e259ac6 100644 --- a/keetanetwork-client-wasm/src/lib.rs +++ b/keetanetwork-client-wasm/src/lib.rs @@ -25,8 +25,8 @@ //! //! // Derive an account (algorithm defaults to ecdsa_secp256k1). //! const me = Account.fromSeed(Account.generateSeed(), 0); -//! const token = Account.fromAddress('keeta_...token...'); -//! const to = Account.fromAddress('keeta_...recipient...'); +//! const token = Account.fromPublicKeyString('keeta_...token...'); +//! const to = Account.fromPublicKeyString('keeta_...recipient...'); //! //! // High-level signed write: send(to, amount, token). //! const user = UserClient.fromClient(client, me); diff --git a/keetanetwork-client-wasm/tests/roundtrip.spec.ts b/keetanetwork-client-wasm/tests/roundtrip.spec.ts index 2ff5ec5..059e414 100644 --- a/keetanetwork-client-wasm/tests/roundtrip.spec.ts +++ b/keetanetwork-client-wasm/tests/roundtrip.spec.ts @@ -31,7 +31,7 @@ interface BuilderRoundTrip { interface GenerationRoundTrip { seedHexLength: number; passphraseWords: number; - derivedAddress: string; + derivedPublicKeyString: string; deterministic: boolean; transmitted: boolean; recipientBalance: string; @@ -86,8 +86,8 @@ test('KeetaClient publishes a send round-trip against a live node', async ({ pag const client = new KeetaClient(cfg.api).withNetwork(cfg.network); const trusted = Account.fromSeed(cfg.trustedSeedHex, 0, 'ed25519'); - const recipient = Account.fromAddress(cfg.recipient); - const token = Account.fromAddress(cfg.baseToken); + const recipient = Account.fromPublicKeyString(cfg.recipient); + const token = Account.fromPublicKeyString(cfg.baseToken); const version = await client.nodeVersion(); const before = await client.balance(trusted, token); @@ -124,14 +124,14 @@ test('UserClient transmits a builder-assembled send', async ({ page }) => { window as unknown as { keeta: typeof Keeta } ).keeta; - const token = Account.fromAddress(cfg.info.baseToken); + const token = Account.fromPublicKeyString(cfg.info.baseToken); const trusted = Account.fromSeed(cfg.info.trustedSeedHex, 0, 'ed25519'); const recipient = Account.fromSeed(cfg.recipientSeedHex, 0, 'ed25519'); const client = new KeetaClient(cfg.info.api).withNetwork(cfg.info.network); const user = UserClient.fromClient(client, trusted); const readOnly = user.isReadOnly; - const signer = user.account().address; + const signer = user.account().publicKeyString; const builder = user.initBuilder(); builder.send(recipient, cfg.info.amount, token); @@ -178,6 +178,31 @@ test('errors cross the boundary with a stable code', async ({ page }) => { expect(code, 'a malformed seed must throw an Error carrying code INVALID_SEED').toBe('INVALID_SEED'); }); +test('the type-prefixed public key hex round-trips an account', async ({ page }) => { + await page.goto('/tests/index.html'); + await page.waitForFunction(() => (window as unknown as { wasmReady?: boolean }).wasmReady === true); + + const result: { keyAndType: string; publicKey: string; address: string; reopenedAddress: string } = + await page.evaluate(async () => { + const { Account } = (window as unknown as { keeta: typeof Keeta }).keeta; + + const account = Account.fromSeed(Account.generateSeed(), 0, 'ed25519'); + const reopened = Account.fromPublicKeyAndType(account.publicKeyAndTypeString); + + return { + keyAndType: account.publicKeyAndTypeString, + publicKey: account.publicKey, + address: account.publicKeyString, + reopenedAddress: reopened.publicKeyString, + }; + }); + + expect(result.keyAndType, 'the getter must match the reference layout: 0x + uppercase key-and-type hex').toBe( + `0x${result.publicKey.toUpperCase()}`, + ); + expect(result.reopenedAddress, 'the reopened account must keep its address').toBe(result.address); +}); + test('a generated account is recoverable and can be funded', async ({ page }) => { const info = await loadNodeInfo(page.request); @@ -187,18 +212,18 @@ test('a generated account is recoverable and can be funded', async ({ page }) => const result: GenerationRoundTrip = await page.evaluate(async (cfg: NodeInfo) => { const { KeetaClient, Account } = (window as unknown as { keeta: typeof Keeta }).keeta; - const token = Account.fromAddress(cfg.baseToken); + const token = Account.fromPublicKeyString(cfg.baseToken); const trusted = Account.fromSeed(cfg.trustedSeedHex, 0, 'ed25519'); // A freshly minted seed must derive a usable, fundable account. const seedHex = Account.generateSeed(); const recipient = Account.fromSeed(seedHex, 0, 'ed25519'); - // A mnemonic must deterministically recover the same address. + // A mnemonic must deterministically recover the same public-key string. const words = Account.generatePassphrase(); const derived = Account.fromPassphrase(words, 0, 'ed25519'); const derivedAgain = Account.fromPassphrase(words, 0, 'ed25519'); - const deterministic = derived.address === derivedAgain.address; + const deterministic = derived.publicKeyString === derivedAgain.publicKeyString; const client = new KeetaClient(cfg.api).withNetwork(cfg.network); const transmitted = await client.send(trusted, recipient, cfg.amount, token); @@ -207,7 +232,7 @@ test('a generated account is recoverable and can be funded', async ({ page }) => return { seedHexLength: seedHex.length, passphraseWords: words.length, - derivedAddress: derived.address, + derivedPublicKeyString: derived.publicKeyString, deterministic, transmitted, recipientBalance, @@ -216,8 +241,8 @@ test('a generated account is recoverable and can be funded', async ({ page }) => expect(result.seedHexLength, 'a generated seed must be 32 bytes of hex').toBe(64); expect(result.passphraseWords, 'a generated mnemonic must be 24 words').toBe(24); - expect(result.derivedAddress, 'a mnemonic-derived account must have an address').toMatch(/^keeta_/); - expect(result.deterministic, 'the same mnemonic must recover the same address').toBe(true); + expect(result.derivedPublicKeyString, 'a mnemonic-derived account must have a public-key string').toMatch(/^keeta_/); + expect(result.deterministic, 'the same mnemonic must recover the same public-key string').toBe(true); expect(result.transmitted, 'the node must accept a send to the generated account').toBe(true); expect(BigInt(result.recipientBalance), 'the send must credit the generated account').toBe(BigInt(info.amount)); }); @@ -319,7 +344,7 @@ test('UserClient builds a swap-request block against a live node', async ({ page async (cfg: { info: NodeInfo; recipientSeedHex: string }) => { const { KeetaClient, UserClient, Account, Block } = (window as unknown as { keeta: typeof Keeta }).keeta; - const token = Account.fromAddress(cfg.info.baseToken); + const token = Account.fromPublicKeyString(cfg.info.baseToken); const trusted = Account.fromSeed(cfg.info.trustedSeedHex, 0, 'ed25519'); const counterparty = Account.fromSeed(cfg.recipientSeedHex, 0, 'ed25519'); @@ -351,10 +376,10 @@ test('reads return structured, typed views with string amounts', async ({ page } const client = new KeetaClient(cfg.api).withNetwork(cfg.network); const trusted = Account.fromSeed(cfg.trustedSeedHex, 0, 'ed25519'); - const token = Account.fromAddress(cfg.baseToken); + const token = Account.fromPublicKeyString(cfg.baseToken); // Drive a send so the trusted account has settled state to read back. - await client.send(trusted, Account.fromAddress(cfg.recipient), cfg.amount, token); + await client.send(trusted, Account.fromPublicKeyString(cfg.recipient), cfg.amount, token); const balances = await client.balances(trusted); let balanceFields: { token: string; balance: string } | null = null; @@ -491,7 +516,7 @@ test.describe('extended client and user surface', () => { const { KeetaClient, UserClient, Account } = (window as unknown as { keeta: typeof Keeta }).keeta; const trusted = Account.fromSeed(cfg.trustedSeedHex, 0, 'ed25519'); - const token = Account.fromAddress(cfg.baseToken); + const token = Account.fromPublicKeyString(cfg.baseToken); const client = new KeetaClient(cfg.api).withNetwork(cfg.network); const user = UserClient.fromClient(client, trusted); @@ -517,11 +542,11 @@ test.describe('extended client and user surface', () => { const { KeetaClient, UserClient, Account } = (window as unknown as { keeta: typeof Keeta }).keeta; const trusted = Account.fromSeed(cfg.trustedSeedHex, 0, 'ed25519'); - const token = Account.fromAddress(cfg.baseToken); + const token = Account.fromPublicKeyString(cfg.baseToken); const client = new KeetaClient(cfg.api).withNetwork(cfg.network); const user = UserClient.fromClient(client, trusted); - await user.send(Account.fromAddress(cfg.recipient), cfg.amount, token); + await user.send(Account.fromPublicKeyString(cfg.recipient), cfg.amount, token); const firstPage = await user.chainPage(undefined, undefined, 1); const everyBlock = await user.chainAll(50); @@ -654,7 +679,7 @@ test.describe('extended client and user surface', () => { const trusted = Account.fromSeed(cfg.trustedSeedHex, 0, 'ed25519'); const recipient = Account.fromSeed(cfg.externalRecipientSeedHex, 0, 'ed25519'); - const token = Account.fromAddress(cfg.baseToken); + const token = Account.fromPublicKeyString(cfg.baseToken); const client = new KeetaClient(cfg.api).withNetwork(cfg.network); const user = UserClient.fromClient(client, trusted); @@ -681,11 +706,11 @@ test.describe('extended client and user surface', () => { const { KeetaClient, UserClient, Account } = (window as unknown as { keeta: typeof Keeta }).keeta; const trusted = Account.fromSeed(cfg.trustedSeedHex, 0, 'ed25519'); - const token = Account.fromAddress(cfg.baseToken); + const token = Account.fromPublicKeyString(cfg.baseToken); const client = new KeetaClient(cfg.api).withNetwork(cfg.network); const user = UserClient.fromClient(client, trusted); - await user.send(Account.fromAddress(cfg.recipient), cfg.amount, token); + await user.send(Account.fromPublicKeyString(cfg.recipient), cfg.amount, token); const head = await user.head(); const headHash = head?.hash ?? ''; @@ -750,7 +775,7 @@ test.describe('extended client and user surface', () => { const discovery = new KeetaClient(cfg.api).withNetwork(cfg.network); const rep = await discovery.nodeRepresentative(); - const repAccount = Account.fromAddress(rep.account); + const repAccount = Account.fromPublicKeyString(rep.account); const endpoint = new RepEndpoint(cfg.api, repAccount, '7'); const client = KeetaClient.forRepresentatives([endpoint]).withNetwork(cfg.network); diff --git a/keetanetwork-client/Cargo.toml b/keetanetwork-client/Cargo.toml index 59f7b6a..be24339 100644 --- a/keetanetwork-client/Cargo.toml +++ b/keetanetwork-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keetanetwork-client" -version = "0.4.0" +version = "0.4.1" edition.workspace = true authors.workspace = true license.workspace = true diff --git a/keetanetwork-client/openapi/keetanet-node.yaml b/keetanetwork-client/openapi/keetanet-node.yaml index 6445f66..0d0d31b 100644 --- a/keetanetwork-client/openapi/keetanet-node.yaml +++ b/keetanetwork-client/openapi/keetanet-node.yaml @@ -196,6 +196,242 @@ components: type: string description: Pending balance as a 0x-prefixed hexadecimal BigInt + CreateVoteResponse: + type: object + properties: + vote: + $ref: '#/components/schemas/Vote' + + CreateVoteQuoteResponse: + type: object + properties: + quote: + $ref: '#/components/schemas/VoteQuote' + + GetBlockVotesResponse: + type: object + properties: + blockhash: + type: string + votes: + type: array + nullable: true + items: + $ref: '#/components/schemas/Vote' + + GetNodeVersionResponse: + type: object + properties: + node: + type: string + + GetAccountStateResponse: + type: object + properties: + account: + type: string + currentHeadBlock: + type: string + description: Head block hash as hexadecimal + currentHeadBlockHeight: + type: string + description: Head block height as a 0x-prefixed hexadecimal BigInt + representative: + type: string + info: + $ref: '#/components/schemas/AccountInfo' + balances: + type: array + items: + $ref: '#/components/schemas/BalanceEntry' + + GetAccountStatesResponseItem: + type: object + properties: + account: + type: string + currentHeadBlock: + type: string + currentHeadBlockHeight: + type: string + representative: + type: string + info: + $ref: '#/components/schemas/AccountInfo' + balances: + type: array + items: + $ref: '#/components/schemas/BalanceEntry' + + GetAccountBalancesResponse: + type: object + properties: + account: + type: string + balances: + type: array + items: + $ref: '#/components/schemas/BalanceEntry' + + GetAccountBalanceResponse: + type: object + properties: + account: + type: string + token: + type: string + balance: + type: string + description: Settled balance as a 0x-prefixed hexadecimal BigInt + + GetAccountHeadResponse: + type: object + properties: + account: + type: string + block: + $ref: '#/components/schemas/Block' + height: + type: string + nullable: true + description: Head block height as a 0x-prefixed hexadecimal BigInt + + GetPendingBlockResponse: + type: object + properties: + account: + type: string + block: + $ref: '#/components/schemas/Block' + + GetBlockResponse: + type: object + properties: + blockhash: + type: string + block: + $ref: '#/components/schemas/Block' + + GetSuccessorBlockResponse: + type: object + properties: + blockhash: + type: string + successorBlock: + $ref: '#/components/schemas/Block' + + GetBlockFromIdempotentResponse: + type: object + properties: + block: + $ref: '#/components/schemas/Block' + + GetLedgerChecksumResponse: + type: object + properties: + moment: + type: string + momentRange: + type: number + checksum: + type: string + description: Checksum as a 0x-prefixed hexadecimal BigInt + + GetAllRepresentativesResponse: + type: object + properties: + representatives: + type: array + items: + $ref: '#/components/schemas/Representative' + + GetGlobalHistoryResponse: + type: object + properties: + history: + type: array + items: + $ref: '#/components/schemas/HistoryEntry' + nextKey: + type: string + + GetAccountChainResponse: + type: object + properties: + account: + type: string + blocks: + type: array + items: + $ref: '#/components/schemas/GetAccountChainResponseBlocksItem' + nextKey: + type: string + + GetAccountChainResponseBlocksItem: + type: object + properties: + block: + $ref: '#/components/schemas/Block' + + GetAccountHistoryResponse: + type: object + properties: + history: + type: array + items: + $ref: '#/components/schemas/HistoryEntry' + nextKey: + type: string + + ListAclsByPrincipalResponse: + type: object + properties: + permissions: + type: array + items: + $ref: '#/components/schemas/ACLRow' + + ListAclsByEntityResponse: + type: object + properties: + permissions: + type: array + items: + $ref: '#/components/schemas/ACLRow' + + GetAccountCertificatesResponse: + type: object + properties: + account: + type: string + certificates: + type: array + items: + $ref: '#/components/schemas/Certificate' + + GetCertificateByHashResponse: + allOf: + - $ref: '#/components/schemas/Certificate' + - type: object + properties: + account: + type: string + + GetVoteStaplesAfterResponse: + type: object + properties: + voteStaples: + type: array + items: + $ref: '#/components/schemas/VoteStaple' + + PublishVoteStapleResponse: + type: object + properties: + publish: + type: boolean + description: Whether the vote staple was successfully published + paths: /vote: @@ -233,10 +469,7 @@ paths: content: application/json: schema: - type: object - properties: - vote: - $ref: '#/components/schemas/Vote' + $ref: '#/components/schemas/CreateVoteResponse' '500': description: Server error content: @@ -271,10 +504,7 @@ paths: content: application/json: schema: - type: object - properties: - quote: - $ref: '#/components/schemas/VoteQuote' + $ref: '#/components/schemas/CreateVoteQuoteResponse' '500': description: Server error content: @@ -311,15 +541,7 @@ paths: content: application/json: schema: - type: object - properties: - blockhash: - type: string - votes: - type: array - nullable: true - items: - $ref: '#/components/schemas/Vote' + $ref: '#/components/schemas/GetBlockVotesResponse' '500': description: Server error content: @@ -340,10 +562,7 @@ paths: content: application/json: schema: - type: object - properties: - node: - type: string + $ref: '#/components/schemas/GetNodeVersionResponse' '500': description: Server error content: @@ -371,24 +590,7 @@ paths: content: application/json: schema: - type: object - properties: - account: - type: string - currentHeadBlock: - type: string - description: Head block hash as hexadecimal - currentHeadBlockHeight: - type: string - description: Head block height as a 0x-prefixed hexadecimal BigInt - representative: - type: string - info: - $ref: '#/components/schemas/AccountInfo' - balances: - type: array - items: - $ref: '#/components/schemas/BalanceEntry' + $ref: '#/components/schemas/GetAccountStateResponse' '500': description: Server error content: @@ -416,14 +618,7 @@ paths: content: application/json: schema: - type: object - properties: - account: - type: string - balances: - type: array - items: - $ref: '#/components/schemas/BalanceEntry' + $ref: '#/components/schemas/GetAccountBalancesResponse' '500': description: Server error content: @@ -457,15 +652,7 @@ paths: content: application/json: schema: - type: object - properties: - account: - type: string - token: - type: string - balance: - type: string - description: Settled balance as a 0x-prefixed hexadecimal BigInt + $ref: '#/components/schemas/GetAccountBalanceResponse' '500': description: Server error content: @@ -493,16 +680,7 @@ paths: content: application/json: schema: - type: object - properties: - account: - type: string - block: - $ref: '#/components/schemas/Block' - height: - type: string - nullable: true - description: Head block height as a 0x-prefixed hexadecimal BigInt + $ref: '#/components/schemas/GetAccountHeadResponse' '500': description: Server error content: @@ -530,12 +708,7 @@ paths: content: application/json: schema: - type: object - properties: - account: - type: string - block: - $ref: '#/components/schemas/Block' + $ref: '#/components/schemas/GetPendingBlockResponse' '500': description: Server error content: @@ -573,12 +746,7 @@ paths: content: application/json: schema: - type: object - properties: - blockhash: - type: string - block: - $ref: '#/components/schemas/Block' + $ref: '#/components/schemas/GetBlockResponse' '500': description: Server error content: @@ -606,12 +774,7 @@ paths: content: application/json: schema: - type: object - properties: - blockhash: - type: string - successorBlock: - $ref: '#/components/schemas/Block' + $ref: '#/components/schemas/GetSuccessorBlockResponse' '500': description: Server error content: @@ -668,15 +831,7 @@ paths: content: application/json: schema: - type: object - properties: - moment: - type: string - momentRange: - type: number - checksum: - type: string - description: Checksum as a 0x-prefixed hexadecimal BigInt + $ref: '#/components/schemas/GetLedgerChecksumResponse' '500': description: Server error content: @@ -739,12 +894,7 @@ paths: content: application/json: schema: - type: object - properties: - representatives: - type: array - items: - $ref: '#/components/schemas/Representative' + $ref: '#/components/schemas/GetAllRepresentativesResponse' '500': description: Server error content: @@ -772,14 +922,7 @@ paths: content: application/json: schema: - type: object - properties: - history: - type: array - items: - $ref: '#/components/schemas/HistoryEntry' - nextKey: - type: string + $ref: '#/components/schemas/GetGlobalHistoryResponse' '500': description: Server error content: @@ -816,19 +959,7 @@ paths: content: application/json: schema: - type: object - properties: - account: - type: string - blocks: - type: array - items: - type: object - properties: - block: - $ref: '#/components/schemas/Block' - nextKey: - type: string + $ref: '#/components/schemas/GetAccountChainResponse' '500': description: Server error content: @@ -861,14 +992,7 @@ paths: content: application/json: schema: - type: object - properties: - history: - type: array - items: - $ref: '#/components/schemas/HistoryEntry' - nextKey: - type: string + $ref: '#/components/schemas/GetAccountHistoryResponse' '500': description: Server error content: @@ -893,12 +1017,7 @@ paths: content: application/json: schema: - type: object - properties: - permissions: - type: array - items: - $ref: '#/components/schemas/ACLRow' + $ref: '#/components/schemas/ListAclsByPrincipalResponse' '500': description: Server error content: @@ -923,12 +1042,7 @@ paths: content: application/json: schema: - type: object - properties: - permissions: - type: array - items: - $ref: '#/components/schemas/ACLRow' + $ref: '#/components/schemas/ListAclsByEntityResponse' '500': description: Server error content: @@ -978,14 +1092,7 @@ paths: content: application/json: schema: - type: object - properties: - account: - type: string - certificates: - type: array - items: - $ref: '#/components/schemas/Certificate' + $ref: '#/components/schemas/GetAccountCertificatesResponse' '500': description: Server error content: @@ -1015,12 +1122,7 @@ paths: content: application/json: schema: - allOf: - - $ref: '#/components/schemas/Certificate' - - type: object - properties: - account: - type: string + $ref: '#/components/schemas/GetCertificateByHashResponse' '500': description: Server error content: @@ -1060,10 +1162,7 @@ paths: content: application/json: schema: - type: object - properties: - block: - $ref: '#/components/schemas/Block' + $ref: '#/components/schemas/GetBlockFromIdempotentResponse' '500': description: Server error content: @@ -1091,22 +1190,7 @@ paths: schema: type: array items: - type: object - properties: - account: - type: string - currentHeadBlock: - type: string - currentHeadBlockHeight: - type: string - representative: - type: string - info: - $ref: '#/components/schemas/AccountInfo' - balances: - type: array - items: - $ref: '#/components/schemas/BalanceEntry' + $ref: '#/components/schemas/GetAccountStatesResponseItem' '500': description: Server error content: @@ -1136,12 +1220,7 @@ paths: content: application/json: schema: - type: object - properties: - voteStaples: - type: array - items: - $ref: '#/components/schemas/VoteStaple' + $ref: '#/components/schemas/GetVoteStaplesAfterResponse' '500': description: Server error content: @@ -1174,11 +1253,7 @@ paths: content: application/json: schema: - type: object - properties: - publish: - type: boolean - description: Whether the vote staple was successfully published + $ref: '#/components/schemas/PublishVoteStapleResponse' '500': description: Server error content: