diff --git a/packages/sdk/docs/specs.md b/packages/sdk/docs/specs.md index 7fa6500bdd..4a88b9baa3 100644 --- a/packages/sdk/docs/specs.md +++ b/packages/sdk/docs/specs.md @@ -1,5 +1,110 @@ # @namada/sdk specs -TODO: Specs for `@namada/sdk` package +The `@namada/sdk` package is a wrapper library to provides an easy way to integrate with the `@namada/shared` WebAssembly Rust library. -[API](./api.md) +View the [API](./api.md) docs for specific information on API interfaces. + +## Table of Contents + +- [Basic Usage](#basic-usage) +- [Keys](#keys) +- [Rpc](#rpc) +- [Tx](#tx) +- [Ledger](#ledger) +- [Masp](#masp) +- [Mnemonic](#mnemonic) +- [Signing](#signing) + +## Basic Usage + +The initialization of the `Sdk` must happen asynchronously for web applications. + +In your application, you can initialize the Sdk with the following: + +```typescript +import { Sdk } from "@namada/sdk"; + +async function myApp() { + const rpcUrl = "http://localhost:27657"; + + // You can use deconstruction to access submodules + const { tx, keys, signing, mnemonic, keys } = await Sdk.init(rpcUrl); + + // ..... +} + +myApp(); +``` + +Alternatively, you can import the `initAsync` function, which will also return an instance of the SDK: + +```typescript +import { initAsync } from "@namada/sdk"; + +async function myApp() { + const rpcUrl = "http://localhost:27657"; + const sdk = await initAsync(rpcUrl); +} + +myApp(); +``` + +[ [Table of Contents](#table-of-contents) ] + +## Keys + +This module contains the functionality to generate transparent and shielded keys for Namada. + +More info _TBD_ + +[ [Table of Contents](#table-of-contents) ] + +## Rpc + +This module contains RPC queries for interacting with the chain, as well as Tx broadcasting. + +More info _TBD_ + +[ [Table of Contents](#table-of-contents) ] + +## Tx + +This module contains functionality for building and signing transactions. + +More info _TBD_ + +[ [Table of Contents](#table-of-contents) ] + +## Ledger + +This class provides functionality for interacting with `NamadaApp` for the Ledger Hardware Wallet. + +More info _TBD_ + +[ [Table of Contents](#table-of-contents) ] + +## Masp + +This module provides a few basic utilities for handling `Masp` params, as well as adding spending keys to the +wallet context. + +More info _TBD_ + +[ [Table of Contents](#table-of-contents) ] + +## Mnemonic + +This provides basic funcitonality for generating 12 or 24 word mnemonics, validating mnemonics, and generating +seeds from mnemonics. + +More info _TBD_ + +[ [Table of Contents](#table-of-contents) ] + +## Signing + +This class provides the functionality from our Rust library for signing and verifying arbitrary data. + +More info _TBD_ + +[ [Table of Contents](#table-of-contents) ] diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index bef0ef8b0e..7f47137579 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,14 +1,17 @@ -// Make Ledger available for direct-import as it is not dependent on Sdk initialization -import { init } from "init"; +import initAsync from "./initAsync"; import { Sdk as SDK } from "./sdk"; +// Make Ledger available for direct-import as it is not dependent on Sdk initialization export * from "./ledger"; // Wrap Sdk export so initialization can happen outside of // class definition (for running tests using wasm built for Node JS) export const Sdk = { + /** + * Initialize Sdk for web applications + */ init: (url: string, token?: string): Promise => { - return init(url, token); + return initAsync(url, token); }, }; @@ -24,3 +27,7 @@ export type { Unbonds, } from "./rpc"; export { EncodedTx, SignedTx } from "./tx"; + +// Export init functions for direct usage +export { default as initAsync } from "./initAsync"; +export { default as initSync } from "./initSync"; diff --git a/packages/sdk/src/init.ts b/packages/sdk/src/initAsync.ts similarity index 87% rename from packages/sdk/src/init.ts rename to packages/sdk/src/initAsync.ts index 482c300ee0..d7ff40905b 100644 --- a/packages/sdk/src/init.ts +++ b/packages/sdk/src/initAsync.ts @@ -5,13 +5,17 @@ import { init as initShared } from "@namada/shared/src/init"; import { Sdk } from "sdk"; /** - * Returns an initialized Sdk class + * Returns an initialized Sdk class asynchronously. This is required to use + * this library in web applications. * @async * @param {string} url - RPC url for use with SDK * @param {string} [token] - Native token of the target chain, if not provided, an attempt to query it will be made * @return {Sdk} Instance of initialized Sdk class */ -export async function init(url: string, token?: string): Promise { +export default async function initAsync( + url: string, + token?: string +): Promise { // Load and initialize shared wasm const sharedWasm = await fetch("shared.namada.wasm").then((wasm) => wasm.arrayBuffer() diff --git a/packages/sdk/src/initSync.ts b/packages/sdk/src/initSync.ts new file mode 100644 index 0000000000..d346592b7c --- /dev/null +++ b/packages/sdk/src/initSync.ts @@ -0,0 +1,17 @@ +import { Query as QueryWasm, Sdk as SdkWasm } from "@namada/shared"; +import { Sdk } from "./sdk"; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const cryptoMemory = require("@namada/crypto").__wasm.memory; + +/** + * Initialize SDK for Node JS environments + * @param {string} url + * @param {string} nativeToken + * @returns {Sdk} + */ +export default function initSync(url: string, nativeToken: string): Sdk { + const sdk = new SdkWasm(url, nativeToken); + const query = new QueryWasm(url); + return new Sdk(sdk, query, cryptoMemory, url, nativeToken); +} diff --git a/packages/sdk/src/keys/keys.ts b/packages/sdk/src/keys/keys.ts index 89da40a487..ae3589b725 100644 --- a/packages/sdk/src/keys/keys.ts +++ b/packages/sdk/src/keys/keys.ts @@ -131,7 +131,10 @@ export class Keys { * @param {Bip44Path} [path={account: 0, change: 0, index: 0}] - Bip44 path object * @returns {ShieldedKeys} */ - deriveShielded(seed: Uint8Array, path: Bip44Path): ShieldedKeys { + deriveShielded( + seed: Uint8Array, + path: Bip44Path = DEFAULT_PATH + ): ShieldedKeys { const { index } = path; const zip32 = ShieldedHDWallet.from_seed(seed); const account = zip32.derive_to_serialized_keys(index); diff --git a/packages/sdk/src/tests/data.ts b/packages/sdk/src/tests/data.ts index c8e468cc5a..d8bdf97acd 100644 --- a/packages/sdk/src/tests/data.ts +++ b/packages/sdk/src/tests/data.ts @@ -21,9 +21,11 @@ export const ACCOUNT_2 = { export const SHIELDED_ACCOUNT = { paymentAddress: - "znam1qz7te22s3790wf53q7v6tcm5tc79xq5dx2h93us5e8pqeadqn63c6rwx2wq4mgdrjnsgxxgkh45wp", - xsk: "", - viewingKey: "", + "znam1qzs8yweqduxya5tlzh858wgsv8d2j4uu2zvha8hgxzt6lzvs4lu2syaxar4eefey42txpkqd6skd5", + spendingKey: + "zsknam1q9slcrk9qqqqqqps3w3yfgaxsw0t0jkzwm5ghlexqdzhz8vulz9cw03daakult7a7m7ud3ld77ax69r6p9h46qet69yp6sa4p4yjwwemg5twrd09usgqk8284zyq9fxsv3u8v2nz737r770xd0ehahwrcpdnzplaecdj95cdzh8tmp8w72yjcg2t47tzcwnkkhn852ru0z4kt87hdxlt9lulzgkchcqes2fuac87l2h0gczfyef7n7uvq3n43pc3thwgrf543cwgjwcyzjrej", + viewingKey: + "zvknam1q9slcrk9qqqqqqps3w3yfgaxsw0t0jkzwm5ghlexqdzhz8vulz9cw03daakult7a7cjwxdh77a36rcs28lvs7emvl9dhcpz2jg57hvjwrau2g9ssstswuyrxq9tpajhe8uxluhakgevlzqcsg2ccleq5qfqtaqz72kvkxek6zh8tmp8w72yjcg2t47tzcwnkkhn852ru0z4kt87hdxlt9lulzgkchcqes2fuac87l2h0gczfyef7n7uvq3n43pc3thwgrf543cwgjwckhrt6j", }; export const SIG_VALID = { diff --git a/packages/sdk/src/tests/initSdk.ts b/packages/sdk/src/tests/initSdk.ts index ec0dc8e171..ebf8bbe9c6 100644 --- a/packages/sdk/src/tests/initSdk.ts +++ b/packages/sdk/src/tests/initSdk.ts @@ -1,13 +1,8 @@ -import { Query as QueryWasm, Sdk as SdkWasm } from "@namada/shared"; +import initSync from "initSync"; import { Sdk } from "../sdk"; import { NATIVE_TOKEN as nativeToken, RPC_URL as rpcUrl } from "./data"; -/* eslint-disable @typescript-eslint/no-var-requires */ -const cryptoMemory = require("@namada/crypto").__wasm.memory; - -export const initSdk = async (): Promise => { - const sdk = new SdkWasm(rpcUrl, nativeToken); - const query = new QueryWasm(rpcUrl); - - return new Sdk(sdk, query, cryptoMemory, rpcUrl, nativeToken); +// Simplified wrapper to handle initializing SDK for tests +export const initSdk = (): Sdk => { + return initSync(rpcUrl, nativeToken); }; diff --git a/packages/sdk/src/tests/keys.test.ts b/packages/sdk/src/tests/keys.test.ts index 88878010f0..637ee4bddb 100644 --- a/packages/sdk/src/tests/keys.test.ts +++ b/packages/sdk/src/tests/keys.test.ts @@ -1,9 +1,13 @@ -import { ACCOUNT_1 as account1, MNEMONIC_1 as mnemonic1 } from "./data"; +import { + ACCOUNT_1 as account1, + MNEMONIC_1 as mnemonic1, + SHIELDED_ACCOUNT as shieldedAccount, +} from "./data"; import { initSdk } from "./initSdk"; describe("Keys", () => { - it("should derive keys from mnemonic phrase", async () => { - const { keys } = await initSdk(); + it("should derive transparent keys from mnemonic phrase", () => { + const { keys } = initSdk(); const { address, publicKey, privateKey } = keys.deriveFromMnemonic(mnemonic1); @@ -13,8 +17,19 @@ describe("Keys", () => { expect(privateKey).toBe(account1.privateKey); }); - it("should derive keys from seed", async () => { - const { keys, mnemonic } = await initSdk(); + it("should derive shielded keys from seed", () => { + const { keys, mnemonic } = initSdk(); + const seed = mnemonic.toSeed(mnemonic1); + + const { address, viewingKey, spendingKey } = keys.deriveShielded(seed); + + expect(address).toBe(shieldedAccount.paymentAddress); + expect(viewingKey).toBe(shieldedAccount.viewingKey); + expect(spendingKey).toBe(shieldedAccount.spendingKey); + }); + + it("should derive keys from seed", () => { + const { keys, mnemonic } = initSdk(); // Generate a seed from a mnemonic phrase const seed = mnemonic.toSeed(mnemonic1); // Derive account from that seed diff --git a/packages/sdk/src/tests/mnemonic.test.ts b/packages/sdk/src/tests/mnemonic.test.ts index d3988457b6..531f6cf7a2 100644 --- a/packages/sdk/src/tests/mnemonic.test.ts +++ b/packages/sdk/src/tests/mnemonic.test.ts @@ -1,16 +1,24 @@ import { PhraseSize } from "@namada/crypto"; +import { MNEMONIC_1 as mnemonic1 } from "./data"; import { initSdk } from "./initSdk"; describe("mnemonic", () => { it("should generate a 12 word mnemonic phrase", async () => { - const { mnemonic } = await initSdk(); + const { mnemonic } = initSdk(); const words = await mnemonic.generate(); expect(words.length).toEqual(12); }); it("should generate a 24 word mnemonic phrase", async () => { - const { mnemonic } = await initSdk(); + const { mnemonic } = initSdk(); const words = await mnemonic.generate(PhraseSize.N24); expect(words.length).toEqual(24); }); + + it("should generate a seed from mnemonic", () => { + const { mnemonic } = initSdk(); + const seed = mnemonic.toSeed(mnemonic1); + expect(seed).toBeDefined(); + expect(seed.length).toEqual(64); + }); }); diff --git a/packages/sdk/src/tests/sdk.test.ts b/packages/sdk/src/tests/sdk.test.ts index 9d40ad6344..009698a51c 100644 --- a/packages/sdk/src/tests/sdk.test.ts +++ b/packages/sdk/src/tests/sdk.test.ts @@ -7,8 +7,8 @@ import { Tx } from "tx"; import { initSdk } from "./initSdk"; describe("Sdk", () => { - it("should initialize Sdk with all sub-components", async () => { - const { tx, keys, mnemonic, rpc, masp, signing } = await initSdk(); + it("should initialize Sdk with all sub-components", () => { + const { tx, keys, mnemonic, rpc, masp, signing } = initSdk(); expect(tx).toBeInstanceOf(Tx); expect(keys).toBeInstanceOf(Keys); expect(mnemonic).toBeInstanceOf(Mnemonic); diff --git a/packages/sdk/src/tests/signing.test.ts b/packages/sdk/src/tests/signing.test.ts index ec0cf2888e..e3a88799bc 100644 --- a/packages/sdk/src/tests/signing.test.ts +++ b/packages/sdk/src/tests/signing.test.ts @@ -6,8 +6,8 @@ import { import { initSdk } from "./initSdk"; describe("Signing", () => { - it("should generate a signature", async () => { - const { signing } = await initSdk(); + it("should generate a signature", () => { + const { signing } = initSdk(); const result = signing.signArbitrary( account1.privateKey, JSON.stringify({ test: "test" }) @@ -18,8 +18,8 @@ describe("Signing", () => { expect(signature).toBe(validSignature.signature); }); - it("should validate a signature", async () => { - const { signing } = await initSdk(); + it("should validate a signature", () => { + const { signing } = initSdk(); const result = signing.verifyArbitrary( account1.publicKey, @@ -30,8 +30,8 @@ describe("Signing", () => { expect(result).toBe(null); }); - it("should throw error when validating an invalid signature", async () => { - const { signing } = await initSdk(); + it("should throw error when validating an invalid signature", () => { + const { signing } = initSdk(); const verify = (): void => signing.verifyArbitrary( diff --git a/packages/sdk/src/tests/tx.test.ts b/packages/sdk/src/tests/tx.test.ts index 61f911b646..56c889620d 100644 --- a/packages/sdk/src/tests/tx.test.ts +++ b/packages/sdk/src/tests/tx.test.ts @@ -14,6 +14,8 @@ describe("Tx", () => { afterAll(() => server.stop()); beforeEach(() => server.reset()); + // TODO: This isn't working. buildTransfer expects a response when it checks whether + // source & target exist. This may apply to other transactions as well. it.skip("should build a transfer tx", async () => { // Mock response for RPC queries that validate that // source and target exist on chain @@ -24,7 +26,7 @@ describe("Tx", () => { ctx.body = true; }); - const { tx } = await initSdk(); + const { tx } = initSdk(); const txProps = { chainId, @@ -48,6 +50,6 @@ describe("Tx", () => { const txBytes = encodedTx.toBytes(); // TODO: Better test here, this is just a placeholder expect(txBytes.length).toEqual(1000); - expect(addressExistsRoute).toHaveBeenCalledTimes(1); + expect(addressExistsRoute).toHaveBeenCalledTimes(2); }); });