diff --git a/.gitignore b/.gitignore index 448e10d0..6f9ec6dc 100644 --- a/.gitignore +++ b/.gitignore @@ -69,4 +69,4 @@ ENV/ .polytest*/ polytest_resources/ -.algokit*/ +.algokit-* diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..6dd77739 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug Vitest Test", + "runtimeExecutable": "npm", + "runtimeArgs": ["test", "--", "--run", "--no-coverage", "${file}"], + "cwd": "${fileDirname}/..", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ] +} diff --git a/package-lock.json b/package-lock.json index ee9e31ac..afd7cd9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10704,7 +10704,12 @@ "name": "@algorandfoundation/algokit-kmd-client", "version": "0.1.0", "license": "MIT", - "devDependencies": {}, + "devDependencies": { + "@algorandfoundation/algokit-algod-client": "*", + "@algorandfoundation/algokit-common": "*", + "@algorandfoundation/algokit-transact": "*", + "zod": "^3.23.8" + }, "engines": { "node": ">=20.0" } diff --git a/package.json b/package.json index 65e3b7eb..9cf9bd36 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,8 @@ "polytest:validate-algod": "polytest --config test_configs/algod_client.jsonc --git 'https://github.com/algorandfoundation/algokit-polytest#main' validate -t vitest", "polytest:generate-algod": "polytest --config test_configs/algod_client.jsonc --git 'https://github.com/algorandfoundation/algokit-polytest#main' generate -t vitest", "polytest:start-mock-servers": "cd .polytest_algokit-polytest/resources/mock-server/scripts/ && ./start_all_servers.sh && cd ../../../..", - "polytest:stop-mock-servers": "cd .polytest_algokit-polytest/resources/mock-server/scripts/ && ./stop_all_servers.sh && cd ../../../.." + "polytest:stop-mock-servers": "cd .polytest_algokit-polytest/resources/mock-server/scripts/ && ./stop_all_servers.sh && cd ../../../..", + "generate:schemas": "npm run generate:schema --workspaces --if-present" }, "overrides": { "esbuild": "0.25.0" diff --git a/packages/kmd_client/package.json b/packages/kmd_client/package.json index 15e1f650..dac6784e 100644 --- a/packages/kmd_client/package.json +++ b/packages/kmd_client/package.json @@ -21,9 +21,15 @@ "check-types": "tsc --noEmit", "audit": "better-npm-audit audit", "format": "prettier --config ../../.prettierrc.cjs --ignore-path ../../.prettierignore --write .", - "pre-commit": "run-s check-types lint:fix audit format test" + "pre-commit": "run-s check-types lint:fix audit format test", + "generate:schema": "tsx ../../scripts/generate-zod-schemas.ts --spec ../../.algokit-oas-generator/specs/kmd.oas3.json --output ./tests/schemas.ts" }, "dependencies": {}, "peerDependencies": {}, - "devDependencies": {} + "devDependencies": { + "@algorandfoundation/algokit-algod-client": "*", + "@algorandfoundation/algokit-common": "*", + "@algorandfoundation/algokit-transact": "*", + "zod": "^3.23.8" + } } diff --git a/packages/kmd_client/tests/config.ts b/packages/kmd_client/tests/config.ts index 5965fbe8..1a2c0a73 100644 --- a/packages/kmd_client/tests/config.ts +++ b/packages/kmd_client/tests/config.ts @@ -15,9 +15,37 @@ function getMockServerUrl(): string { return process.env.MOCK_KMD_URL || process.env.MOCK_KMD_SERVER || `http://127.0.0.1:${MOCK_PORTS.kmd.host}` } +// Mock server configuration export const config: ClientConfig = { baseUrl: getMockServerUrl(), token: process.env.MOCK_KMD_TOKEN || DEFAULT_TOKEN, } +// Localnet KMD configuration +const kmdServer = process.env.KMD_SERVER || 'http://localhost' +const kmdPort = process.env.KMD_PORT || '4002' +const kmdBaseUrl = `${kmdServer}:${kmdPort}` + +export const localnetConfig: ClientConfig = { + baseUrl: kmdBaseUrl, + token: process.env.KMD_TOKEN || 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', +} + +// Localnet Algod configuration +const algodServer = process.env.ALGOD_SERVER || 'http://localhost' +const algodPort = process.env.ALGOD_PORT || '4001' +const algodBaseUrl = `${algodServer}:${algodPort}` + +export const localnetAlgodConfig = { + baseUrl: algodBaseUrl, + token: process.env.ALGOD_TOKEN || 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', +} + +// Test constants +export const TEST_WALLET_PASSWORD = 'test-password-123' +export const TEST_WALLET_DRIVER = 'sqlite' +export const MULTISIG_VERSION = 1 +export const MULTISIG_THRESHOLD = 2 +export const MULTISIG_KEY_COUNT = 3 + export { TEST_ADDRESS, TEST_APP_ID, TEST_APP_ID_WITH_BOXES, TEST_BOX_NAME, TEST_ASSET_ID, TEST_TXID, TEST_ROUND } diff --git a/packages/kmd_client/tests/delete_v1_key.test.ts b/packages/kmd_client/tests/delete_v1_key.test.ts new file mode 100644 index 00000000..052670c5 --- /dev/null +++ b/packages/kmd_client/tests/delete_v1_key.test.ts @@ -0,0 +1,42 @@ +import { Address } from '@algorandfoundation/algokit-common' +import { describe, expect, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetConfig, TEST_WALLET_PASSWORD } from './config' +import { generateTestKey, getWalletHandle, releaseWalletHandle } from './fixtures' + +describe('DELETE v1_key', () => { + // Polytest Suite: DELETE v1_key + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + const { walletHandleToken } = await getWalletHandle(client) + + try { + // Generate a key to delete + const addressStr = await generateTestKey(client, walletHandleToken) + + // Verify key exists + const listBefore = await client.listKeysInWallet({ walletHandleToken }) + expect(listBefore.addresses.map((a) => a.toString())).toContain(addressStr) + + // Delete the key (returns void) + await expect( + client.deleteKey({ + address: Address.fromString(addressStr), + walletHandleToken, + walletPassword: TEST_WALLET_PASSWORD, + }), + ).resolves.toBeUndefined() + + // Verify key was deleted + const listAfter = await client.listKeysInWallet({ walletHandleToken }) + expect(listAfter.addresses.map((a) => a.toString())).not.toContain(addressStr) + } finally { + await releaseWalletHandle(client, walletHandleToken) + } + }) + }) +}) \ No newline at end of file diff --git a/packages/kmd_client/tests/delete_v1_multisig.test.ts b/packages/kmd_client/tests/delete_v1_multisig.test.ts new file mode 100644 index 00000000..c8ce9d50 --- /dev/null +++ b/packages/kmd_client/tests/delete_v1_multisig.test.ts @@ -0,0 +1,42 @@ +import { Address } from '@algorandfoundation/algokit-common' +import { describe, expect, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetConfig, TEST_WALLET_PASSWORD } from './config' +import { createTestMultisig, getWalletHandle, releaseWalletHandle } from './fixtures' + +describe('DELETE v1_multisig', () => { + // Polytest Suite: DELETE v1_multisig + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + const { walletHandleToken } = await getWalletHandle(client) + + try { + // Create a multisig first + const { multisigAddress } = await createTestMultisig(client, walletHandleToken) + + // Verify multisig exists + const listBefore = await client.listMultisig({ walletHandleToken }) + expect(listBefore.addresses.map((a) => a.toString())).toContain(multisigAddress) + + // Delete the multisig (returns void) + await expect( + client.deleteMultisig({ + address: Address.fromString(multisigAddress), + walletHandleToken, + walletPassword: TEST_WALLET_PASSWORD, + }), + ).resolves.toBeUndefined() + + // Verify multisig was deleted + const listAfter = await client.listMultisig({ walletHandleToken }) + expect(listAfter.addresses.map((a) => a.toString())).not.toContain(multisigAddress) + } finally { + await releaseWalletHandle(client, walletHandleToken) + } + }) + }) +}) \ No newline at end of file diff --git a/packages/kmd_client/tests/fixtures.ts b/packages/kmd_client/tests/fixtures.ts new file mode 100644 index 00000000..a50f0bbc --- /dev/null +++ b/packages/kmd_client/tests/fixtures.ts @@ -0,0 +1,144 @@ +import { decodeAddress, encodeAddress } from '@algorandfoundation/algokit-common' +import type { KmdClient } from '../src/client' +import { MULTISIG_KEY_COUNT, MULTISIG_THRESHOLD, MULTISIG_VERSION, TEST_WALLET_DRIVER, TEST_WALLET_PASSWORD } from './config' + +/** + * Generates a unique wallet name for testing + */ +export function generateWalletName(): string { + return `test-wallet-${Date.now()}-${Math.random().toString(36).substring(7)}` +} + +/** + * Creates a test wallet and returns wallet ID and name + */ +export async function createTestWallet( + client: KmdClient, + password: string = TEST_WALLET_PASSWORD, +): Promise<{ walletId: string; walletName: string }> { + const walletName = generateWalletName() + const result = await client.createWallet({ + walletName, + walletPassword: password, + walletDriverName: TEST_WALLET_DRIVER, + }) + return { + walletId: result.wallet.id, + walletName: result.wallet.name, + } +} + +/** + * Creates a wallet and initializes a wallet handle token (unlocks wallet) + */ +export async function getWalletHandle( + client: KmdClient, + password: string = TEST_WALLET_PASSWORD, +): Promise<{ + walletHandleToken: string + walletId: string + walletName: string +}> { + // Create wallet + const { walletId, walletName } = await createTestWallet(client, password) + + // Initialize handle (unlock) + const initResult = await client.initWalletHandle({ + walletId, + walletPassword: password, + }) + + return { + walletHandleToken: initResult.walletHandleToken, + walletId, + walletName, + } +} + +/** + * Releases a wallet handle token (locks wallet) + * Used for cleanup in tests + */ +export async function releaseWalletHandle(client: KmdClient, walletHandleToken: string): Promise { + try { + await client.releaseWalletHandleToken({ walletHandleToken }) + } catch (error) { + // Ignore errors during cleanup (handle may have already expired) + console.warn('Failed to release wallet handle:', error) + } +} + +/** + * Generates a key in the wallet and returns the address + */ +export async function generateTestKey(client: KmdClient, walletHandleToken: string): Promise { + const result = await client.generateKey({ + walletHandleToken, + }) + return result.address.toString() +} + +/** + * Generates multiple keys for multisig tests + */ +export async function generateMultipleKeys( + client: KmdClient, + walletHandleToken: string, + count: number = MULTISIG_KEY_COUNT, +): Promise { + const addresses: string[] = [] + for (let i = 0; i < count; i++) { + const address = await generateTestKey(client, walletHandleToken) + addresses.push(address) + } + return addresses +} + +/** + * Converts an Algorand address string to a public key (Uint8Array) + */ +export function addressToPublicKey(address: string): Uint8Array { + return decodeAddress(address).publicKey +} + +/** + * Converts a public key (Uint8Array) to an Algorand address string + */ +export function publicKeyToAddress(publicKey: Uint8Array): string { + return encodeAddress(publicKey) +} + +/** + * Creates a multisig account with test keys + */ +export async function createTestMultisig( + client: KmdClient, + walletHandleToken: string, + threshold: number = MULTISIG_THRESHOLD, + keyCount: number = MULTISIG_KEY_COUNT, +): Promise<{ + multisigAddress: string + publicKeys: Uint8Array[] + addresses: string[] + threshold: number +}> { + // Generate keys + const addresses = await generateMultipleKeys(client, walletHandleToken, keyCount) + + const publicKeys = addresses.map((addr) => addressToPublicKey(addr)) + + // Import multisig + const result = await client.importMultisig({ + walletHandleToken, + multisigVersion: MULTISIG_VERSION, + threshold: threshold, + publicKeys: publicKeys, + }) + + return { + multisigAddress: result.address.toString(), + publicKeys, + addresses, + threshold, + } +} diff --git a/packages/kmd_client/tests/get_v1_wallets.test.ts b/packages/kmd_client/tests/get_v1_wallets.test.ts new file mode 100644 index 00000000..210c9eb3 --- /dev/null +++ b/packages/kmd_client/tests/get_v1_wallets.test.ts @@ -0,0 +1,20 @@ +import { describe, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetConfig } from './config' +import { ListWalletsResponse } from './schemas' + +describe('GET v1_wallets', () => { + // Polytest Suite: GET v1_wallets + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + + const result = await client.listWallets() + + ListWalletsResponse.parse(result) + }) + }) +}) \ No newline at end of file diff --git a/packages/kmd_client/tests/get_versions.test.ts b/packages/kmd_client/tests/get_versions.test.ts new file mode 100644 index 00000000..223a7ce8 --- /dev/null +++ b/packages/kmd_client/tests/get_versions.test.ts @@ -0,0 +1,20 @@ +import { describe, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetConfig } from './config' +import { VersionsResponse } from './schemas' + +describe('GET versions', () => { + // Polytest Suite: GET versions + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + + const result = await client.version() + + VersionsResponse.parse(result) + }) + }) +}) diff --git a/packages/kmd_client/tests/post_v1_key.test.ts b/packages/kmd_client/tests/post_v1_key.test.ts new file mode 100644 index 00000000..0716512d --- /dev/null +++ b/packages/kmd_client/tests/post_v1_key.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetConfig } from './config' +import { getWalletHandle, releaseWalletHandle } from './fixtures' +import { GenerateKeyResponse } from './schemas' + +describe('POST v1_key', () => { + // Polytest Suite: POST v1_key + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + const { walletHandleToken } = await getWalletHandle(client) + + try { + const result = await client.generateKey({ + walletHandleToken, + }) + + GenerateKeyResponse.parse(result) + + // Verify the generated address has valid Algorand address format + const addressString = result.address.toString() + + // Verify the key exists in the wallet + const listResult = await client.listKeysInWallet({ walletHandleToken }) + const addresses = listResult.addresses.map((addr) => addr.toString()) + expect(addresses).toContain(addressString) + } finally { + await releaseWalletHandle(client, walletHandleToken) + } + }) + }) +}) diff --git a/packages/kmd_client/tests/post_v1_key_export.test.ts b/packages/kmd_client/tests/post_v1_key_export.test.ts new file mode 100644 index 00000000..cc14bb8a --- /dev/null +++ b/packages/kmd_client/tests/post_v1_key_export.test.ts @@ -0,0 +1,34 @@ +import { Address } from '@algorandfoundation/algokit-common' +import { describe, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetConfig, TEST_WALLET_PASSWORD } from './config' +import { generateTestKey, getWalletHandle, releaseWalletHandle } from './fixtures' +import { ExportKeyResponse } from './schemas' + +describe('POST v1_key_export', () => { + // Polytest Suite: POST v1_key_export + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + const { walletHandleToken } = await getWalletHandle(client) + + try { + // Generate a key first + const addressStr = await generateTestKey(client, walletHandleToken) + + const result = await client.exportKey({ + walletHandleToken, + address: Address.fromString(addressStr), + walletPassword: TEST_WALLET_PASSWORD, + }) + + ExportKeyResponse.parse(result) + } finally { + await releaseWalletHandle(client, walletHandleToken) + } + }) + }) +}) \ No newline at end of file diff --git a/packages/kmd_client/tests/post_v1_key_import.test.ts b/packages/kmd_client/tests/post_v1_key_import.test.ts new file mode 100644 index 00000000..e6867bc7 --- /dev/null +++ b/packages/kmd_client/tests/post_v1_key_import.test.ts @@ -0,0 +1,39 @@ +import { randomBytes } from 'crypto' +import nacl from 'tweetnacl' +import { describe, expect, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetConfig } from './config' +import { getWalletHandle, publicKeyToAddress, releaseWalletHandle } from './fixtures' +import { ImportKeyResponse } from './schemas' + +describe('POST v1_key_import', () => { + // Polytest Suite: POST v1_key_import + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + const { walletHandleToken } = await getWalletHandle(client) + + try { + // Generate a random ed25519 keypair + const seed = randomBytes(32) + const keyPair = nacl.sign.keyPair.fromSeed(seed) + + const result = await client.importKey({ + walletHandleToken, + privateKey: keyPair.secretKey, + }) + + ImportKeyResponse.parse(result) + + // Verify the imported key's address matches the public key + const expectedAddress = publicKeyToAddress(keyPair.publicKey) + expect(result.address.toString()).toBe(expectedAddress) + } finally { + await releaseWalletHandle(client, walletHandleToken) + } + }) + }) +}) diff --git a/packages/kmd_client/tests/post_v1_key_list.test.ts b/packages/kmd_client/tests/post_v1_key_list.test.ts new file mode 100644 index 00000000..4a585a15 --- /dev/null +++ b/packages/kmd_client/tests/post_v1_key_list.test.ts @@ -0,0 +1,31 @@ +import { describe, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetConfig } from './config' +import { generateTestKey, getWalletHandle, releaseWalletHandle } from './fixtures' +import { ListKeysResponse } from './schemas' + +describe('POST v1_key_list', () => { + // Polytest Suite: POST v1_key_list + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + const { walletHandleToken } = await getWalletHandle(client) + + try { + // Generate at least one key + await generateTestKey(client, walletHandleToken) + + const result = await client.listKeysInWallet({ + walletHandleToken, + }) + + ListKeysResponse.parse(result) + } finally { + await releaseWalletHandle(client, walletHandleToken) + } + }) + }) +}) \ No newline at end of file diff --git a/packages/kmd_client/tests/post_v1_master_key_export.test.ts b/packages/kmd_client/tests/post_v1_master_key_export.test.ts new file mode 100644 index 00000000..8ebab977 --- /dev/null +++ b/packages/kmd_client/tests/post_v1_master_key_export.test.ts @@ -0,0 +1,29 @@ +import { describe, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetConfig, TEST_WALLET_PASSWORD } from './config' +import { getWalletHandle, releaseWalletHandle } from './fixtures' +import { ExportMasterKeyResponse } from './schemas' + +describe('POST v1_master-key_export', () => { + // Polytest Suite: POST v1_master-key_export + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + const { walletHandleToken } = await getWalletHandle(client) + + try { + const result = await client.exportMasterKey({ + walletHandleToken, + walletPassword: TEST_WALLET_PASSWORD, + }) + + ExportMasterKeyResponse.parse(result) + } finally { + await releaseWalletHandle(client, walletHandleToken) + } + }) + }) +}) diff --git a/packages/kmd_client/tests/post_v1_multisig_export.test.ts b/packages/kmd_client/tests/post_v1_multisig_export.test.ts new file mode 100644 index 00000000..26c06991 --- /dev/null +++ b/packages/kmd_client/tests/post_v1_multisig_export.test.ts @@ -0,0 +1,33 @@ +import { Address } from '@algorandfoundation/algokit-common' +import { describe, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetConfig } from './config' +import { createTestMultisig, getWalletHandle, releaseWalletHandle } from './fixtures' +import { ExportMultisigResponse } from './schemas' + +describe('POST v1_multisig_export', () => { + // Polytest Suite: POST v1_multisig_export + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + const { walletHandleToken } = await getWalletHandle(client) + + try { + // Create a multisig first + const { multisigAddress } = await createTestMultisig(client, walletHandleToken) + + const result = await client.exportMultisig({ + walletHandleToken, + address: Address.fromString(multisigAddress), + }) + + ExportMultisigResponse.parse(result) + } finally { + await releaseWalletHandle(client, walletHandleToken) + } + }) + }) +}) diff --git a/packages/kmd_client/tests/post_v1_multisig_import.test.ts b/packages/kmd_client/tests/post_v1_multisig_import.test.ts new file mode 100644 index 00000000..a9ee2bfe --- /dev/null +++ b/packages/kmd_client/tests/post_v1_multisig_import.test.ts @@ -0,0 +1,35 @@ +import { describe, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetConfig, MULTISIG_KEY_COUNT, MULTISIG_THRESHOLD, MULTISIG_VERSION } from './config' +import { addressToPublicKey, generateMultipleKeys, getWalletHandle, releaseWalletHandle } from './fixtures' +import { ImportMultisigResponse } from './schemas' + +describe('POST v1_multisig_import', () => { + // Polytest Suite: POST v1_multisig_import + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + const { walletHandleToken } = await getWalletHandle(client) + + try { + // Generate keys for multisig + const addresses = await generateMultipleKeys(client, walletHandleToken, MULTISIG_KEY_COUNT) + const publicKeys = addresses.map((addr) => addressToPublicKey(addr)) + + const result = await client.importMultisig({ + walletHandleToken, + multisigVersion: MULTISIG_VERSION, + threshold: MULTISIG_THRESHOLD, + publicKeys: publicKeys, + }) + + ImportMultisigResponse.parse(result) + } finally { + await releaseWalletHandle(client, walletHandleToken) + } + }) + }) +}) \ No newline at end of file diff --git a/packages/kmd_client/tests/post_v1_multisig_list.test.ts b/packages/kmd_client/tests/post_v1_multisig_list.test.ts new file mode 100644 index 00000000..1f0eec97 --- /dev/null +++ b/packages/kmd_client/tests/post_v1_multisig_list.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetConfig } from './config' +import { createTestMultisig, getWalletHandle, releaseWalletHandle } from './fixtures' +import { ListMultisigResponse } from './schemas' + +describe('POST v1_multisig_list', () => { + // Polytest Suite: POST v1_multisig_list + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + const { walletHandleToken } = await getWalletHandle(client) + + try { + // Create a multisig first + const { multisigAddress } = await createTestMultisig(client, walletHandleToken) + + const result = await client.listMultisig({ + walletHandleToken, + }) + + ListMultisigResponse.parse(result) + + // Verify the multisig is in the list + expect(result.addresses.map((a) => a.toString())).toContain(multisigAddress) + } finally { + await releaseWalletHandle(client, walletHandleToken) + } + }) + }) +}) \ No newline at end of file diff --git a/packages/kmd_client/tests/post_v1_multisig_sign.test.ts b/packages/kmd_client/tests/post_v1_multisig_sign.test.ts new file mode 100644 index 00000000..7bddbcd2 --- /dev/null +++ b/packages/kmd_client/tests/post_v1_multisig_sign.test.ts @@ -0,0 +1,56 @@ +import { Address } from '@algorandfoundation/algokit-common' +import { AlgodClient } from '@algorandfoundation/algokit-algod-client' +import { Transaction, TransactionType } from '@algorandfoundation/algokit-transact' +import { describe, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetAlgodConfig, localnetConfig, TEST_WALLET_PASSWORD } from './config' +import { createTestMultisig, getWalletHandle, releaseWalletHandle } from './fixtures' +import { SignMultisigResponse } from './schemas' + +describe('POST v1_multisig_sign', () => { + // Polytest Suite: POST v1_multisig_sign + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + const algodClient = new AlgodClient(localnetAlgodConfig) + const { walletHandleToken } = await getWalletHandle(client) + + try { + // Create a multisig account + const { multisigAddress, publicKeys } = await createTestMultisig(client, walletHandleToken) + + // Get suggested params from algod + const suggestedParams = await algodClient.suggestedParams() + + // Create a simple payment transaction from the multisig address + const transaction = new Transaction({ + type: TransactionType.Payment, + sender: Address.fromString(multisigAddress), + firstValid: suggestedParams.firstValid, + lastValid: suggestedParams.lastValid, + genesisHash: suggestedParams.genesisHash, + genesisId: suggestedParams.genesisId, + payment: { + receiver: Address.fromString(multisigAddress), // Self-payment + amount: 0n, + }, + }) + + // Sign with the first key + const result = await client.signMultisigTransaction({ + walletHandleToken, + transaction, + publicKey: publicKeys[0], + walletPassword: TEST_WALLET_PASSWORD, + }) + + SignMultisigResponse.parse(result) + } finally { + await releaseWalletHandle(client, walletHandleToken) + } + }) + }) +}) \ No newline at end of file diff --git a/packages/kmd_client/tests/post_v1_multisig_signprogram.test.ts b/packages/kmd_client/tests/post_v1_multisig_signprogram.test.ts new file mode 100644 index 00000000..fc4732e6 --- /dev/null +++ b/packages/kmd_client/tests/post_v1_multisig_signprogram.test.ts @@ -0,0 +1,44 @@ +import { AlgodClient } from '@algorandfoundation/algokit-algod-client' +import { Address } from '@algorandfoundation/algokit-common' +import { describe, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetAlgodConfig, localnetConfig, TEST_WALLET_PASSWORD } from './config' +import { createTestMultisig, getWalletHandle, releaseWalletHandle } from './fixtures' +import { SignProgramMultisigResponse } from './schemas' + +describe('POST v1_multisig_signprogram', () => { + // Polytest Suite: POST v1_multisig_signprogram + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + const algodClient = new AlgodClient(localnetAlgodConfig) + const { walletHandleToken } = await getWalletHandle(client) + + try { + // Create a multisig account + const { multisigAddress, publicKeys } = await createTestMultisig(client, walletHandleToken) + + // Compile a simple TEAL program (always approves) + const tealSource = '#pragma version 8\nint 1' + const compileResult = await algodClient.tealCompile(tealSource) + const programBytes = new Uint8Array(Buffer.from(compileResult.result, 'base64')) + + // Sign the program with the first key + const result = await client.signMultisigProgram({ + walletHandleToken, + address: Address.fromString(multisigAddress), + program: programBytes, + publicKey: publicKeys[0], + walletPassword: TEST_WALLET_PASSWORD, + }) + + SignProgramMultisigResponse.parse(result) + } finally { + await releaseWalletHandle(client, walletHandleToken) + } + }) + }) +}) diff --git a/packages/kmd_client/tests/post_v1_program_sign.test.ts b/packages/kmd_client/tests/post_v1_program_sign.test.ts new file mode 100644 index 00000000..59087bc1 --- /dev/null +++ b/packages/kmd_client/tests/post_v1_program_sign.test.ts @@ -0,0 +1,46 @@ +import { Address } from '@algorandfoundation/algokit-common' +import { Buffer } from 'buffer' +import { AlgodClient } from '@algorandfoundation/algokit-algod-client' +import { describe, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetAlgodConfig, localnetConfig, TEST_WALLET_PASSWORD } from './config' +import { generateTestKey, getWalletHandle, releaseWalletHandle } from './fixtures' +import { SignProgramResponse } from './schemas' + +describe('POST v1_program_sign', () => { + // Polytest Suite: POST v1_program_sign + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + const algodClient = new AlgodClient(localnetAlgodConfig) + const { walletHandleToken } = await getWalletHandle(client) + + try { + // Generate a key + const addressStr = await generateTestKey(client, walletHandleToken) + + // Compile a simple TEAL program (always approves) + const tealSource = '#pragma version 8\nint 1' + const compileResult = await algodClient.tealCompile(tealSource) + + // Decode base64 result to Uint8Array + const programBytes = new Uint8Array(Buffer.from(compileResult.result, 'base64')) + + // Sign the program + const result = await client.signProgram({ + walletHandleToken, + address: Address.fromString(addressStr), + program: programBytes, + walletPassword: TEST_WALLET_PASSWORD, + }) + + SignProgramResponse.parse(result) + } finally { + await releaseWalletHandle(client, walletHandleToken) + } + }) + }) +}) \ No newline at end of file diff --git a/packages/kmd_client/tests/post_v1_transaction_sign.test.ts b/packages/kmd_client/tests/post_v1_transaction_sign.test.ts new file mode 100644 index 00000000..ff965cbe --- /dev/null +++ b/packages/kmd_client/tests/post_v1_transaction_sign.test.ts @@ -0,0 +1,55 @@ +import { Address } from '@algorandfoundation/algokit-common' +import { AlgodClient } from '@algorandfoundation/algokit-algod-client' +import { Transaction, TransactionType } from '@algorandfoundation/algokit-transact' +import { describe, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetAlgodConfig, localnetConfig, TEST_WALLET_PASSWORD } from './config' +import { generateTestKey, getWalletHandle, releaseWalletHandle } from './fixtures' +import { SignTransactionResponse } from './schemas' + +describe('POST v1_transaction_sign', () => { + // Polytest Suite: POST v1_transaction_sign + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + const algodClient = new AlgodClient(localnetAlgodConfig) + const { walletHandleToken } = await getWalletHandle(client) + + try { + // Generate a key + const addressStr = await generateTestKey(client, walletHandleToken) + + // Get suggested params from algod + const suggestedParams = await algodClient.suggestedParams() + + // Create a simple payment transaction + const transaction = new Transaction({ + type: TransactionType.Payment, + sender: Address.fromString(addressStr), + firstValid: suggestedParams.firstValid, + lastValid: suggestedParams.lastValid, + genesisHash: suggestedParams.genesisHash, + genesisId: suggestedParams.genesisId, + payment: { + receiver: Address.fromString(addressStr), // Self-payment + amount: 0n, + }, + }) + + // Sign the transaction + const result = await client.signTransaction({ + walletHandleToken, + transaction, + walletPassword: TEST_WALLET_PASSWORD, + }) + + SignTransactionResponse.parse(result) + } finally { + await releaseWalletHandle(client, walletHandleToken) + } + }) + }) +}) \ No newline at end of file diff --git a/packages/kmd_client/tests/post_v1_wallet.test.ts b/packages/kmd_client/tests/post_v1_wallet.test.ts new file mode 100644 index 00000000..5efd67e2 --- /dev/null +++ b/packages/kmd_client/tests/post_v1_wallet.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetConfig, TEST_WALLET_DRIVER, TEST_WALLET_PASSWORD } from './config' +import { generateWalletName } from './fixtures' +import { CreateWalletResponse } from './schemas' + +describe('POST v1_wallet', () => { + // Polytest Suite: POST v1_wallet + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + + const walletName = generateWalletName() + const result = await client.createWallet({ + walletName, + walletPassword: TEST_WALLET_PASSWORD, + walletDriverName: TEST_WALLET_DRIVER, + }) + + CreateWalletResponse.parse(result) + + // Verify the wallet was created + const listResult = await client.listWallets() + const createdWallet = listResult.wallets.find((w) => w.id === result.wallet.id) + expect(createdWallet).toBeDefined() + expect(createdWallet?.name).toBe(walletName) + }) + }) +}) diff --git a/packages/kmd_client/tests/post_v1_wallet_info.test.ts b/packages/kmd_client/tests/post_v1_wallet_info.test.ts new file mode 100644 index 00000000..dbe2cac3 --- /dev/null +++ b/packages/kmd_client/tests/post_v1_wallet_info.test.ts @@ -0,0 +1,28 @@ +import { describe, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetConfig } from './config' +import { getWalletHandle, releaseWalletHandle } from './fixtures' +import { WalletInfoResponse } from './schemas' + +describe('POST v1_wallet_info', () => { + // Polytest Suite: POST v1_wallet_info + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + const { walletHandleToken } = await getWalletHandle(client) + + try { + const result = await client.walletInfo({ + walletHandleToken, + }) + + WalletInfoResponse.parse(result) + } finally { + await releaseWalletHandle(client, walletHandleToken) + } + }) + }) +}) diff --git a/packages/kmd_client/tests/post_v1_wallet_init.test.ts b/packages/kmd_client/tests/post_v1_wallet_init.test.ts new file mode 100644 index 00000000..7603dd46 --- /dev/null +++ b/packages/kmd_client/tests/post_v1_wallet_init.test.ts @@ -0,0 +1,27 @@ +import { describe, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetConfig, TEST_WALLET_PASSWORD } from './config' +import { createTestWallet } from './fixtures' +import { InitWalletHandleTokenResponse } from './schemas' + +describe('POST v1_wallet_init', () => { + // Polytest Suite: POST v1_wallet_init + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + + // Create a wallet first + const { walletId } = await createTestWallet(client) + + const result = await client.initWalletHandle({ + walletId, + walletPassword: TEST_WALLET_PASSWORD, + }) + + InitWalletHandleTokenResponse.parse(result) + }) + }) +}) \ No newline at end of file diff --git a/packages/kmd_client/tests/post_v1_wallet_release.test.ts b/packages/kmd_client/tests/post_v1_wallet_release.test.ts new file mode 100644 index 00000000..1ddae172 --- /dev/null +++ b/packages/kmd_client/tests/post_v1_wallet_release.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetConfig } from './config' +import { getWalletHandle } from './fixtures' + +describe('POST v1_wallet_release', () => { + // Polytest Suite: POST v1_wallet_release + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + const { walletHandleToken } = await getWalletHandle(client) + + // Assert that releaseWalletHandleToken returns undefined + await expect( + client.releaseWalletHandleToken({ + walletHandleToken, + }), + ).resolves.toBeUndefined() + + // Verify the handle is now invalid by trying to use it + await expect(client.walletInfo({ walletHandleToken })).rejects.toThrow() + }) + }) +}) diff --git a/packages/kmd_client/tests/post_v1_wallet_rename.test.ts b/packages/kmd_client/tests/post_v1_wallet_rename.test.ts new file mode 100644 index 00000000..b23f31af --- /dev/null +++ b/packages/kmd_client/tests/post_v1_wallet_rename.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetConfig, TEST_WALLET_PASSWORD } from './config' +import { getWalletHandle, releaseWalletHandle } from './fixtures' +import { RenameWalletResponse } from './schemas' + +describe('POST v1_wallet_rename', () => { + // Polytest Suite: POST v1_wallet_rename + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + const { walletHandleToken, walletId, walletName } = await getWalletHandle(client) + + try { + const newWalletName = `${walletName}-renamed` + const result = await client.renameWallet({ + walletId, + walletPassword: TEST_WALLET_PASSWORD, + walletName: newWalletName, + }) + + RenameWalletResponse.parse(result) + + // Verify the wallet was renamed + const walletInfo = await client.walletInfo({ walletHandleToken }) + expect(walletInfo.walletHandle.wallet.name).toBe(newWalletName) + } finally { + await releaseWalletHandle(client, walletHandleToken) + } + }) + }) +}) diff --git a/packages/kmd_client/tests/post_v1_wallet_renew.test.ts b/packages/kmd_client/tests/post_v1_wallet_renew.test.ts new file mode 100644 index 00000000..f39fe55b --- /dev/null +++ b/packages/kmd_client/tests/post_v1_wallet_renew.test.ts @@ -0,0 +1,28 @@ +import { describe, test } from 'vitest' +import { KmdClient } from '../src/client' +import { localnetConfig } from './config' +import { getWalletHandle, releaseWalletHandle } from './fixtures' +import { RenewWalletHandleTokenResponse } from './schemas' + +describe('POST v1_wallet_renew', () => { + // Polytest Suite: POST v1_wallet_renew + + describe('Common Tests', () => { + // Polytest Group: Common Tests + + test('Basic request and response validation', async () => { + const client = new KmdClient(localnetConfig) + const { walletHandleToken } = await getWalletHandle(client) + + try { + const result = await client.renewWalletHandleToken({ + walletHandleToken, + }) + + RenewWalletHandleTokenResponse.parse(result) + } finally { + await releaseWalletHandle(client, walletHandleToken) + } + }) + }) +}) \ No newline at end of file diff --git a/packages/kmd_client/tests/schemas.ts b/packages/kmd_client/tests/schemas.ts new file mode 100644 index 00000000..d8d4f746 --- /dev/null +++ b/packages/kmd_client/tests/schemas.ts @@ -0,0 +1,249 @@ +/** + * Auto-generated Zod schemas from OpenAPI specification. + * Do not edit manually. + * + * Generated: 2025-12-18T17:23:47.961Z + */ + +import { z } from 'zod' +import { Address } from '@algorandfoundation/algokit-common' + +export const TxType = z.string() + +export const Wallet = z.object({ + driverName: z.string(), + driverVersion: z.number().int(), + id: z.string(), + mnemonicUx: z.boolean(), + name: z.string(), + supportedTxs: z.array(TxType) +}) + +export const ListWalletsResponse = z.object({ + wallets: z.array(Wallet) +}) + +export const ExportKeyResponse = z.object({ + privateKey: z.instanceof(Uint8Array) +}) + +export const ImportKeyResponse = z.object({ + address: z.instanceof(Address) +}) + +export const ListKeysResponse = z.object({ + addresses: z.array(z.instanceof(Address)) +}) + +export const GenerateKeyResponse = z.object({ + address: z.instanceof(Address) +}) + +export const MasterDerivationKey = z.instanceof(Uint8Array) + +export const ExportMasterKeyResponse = z.object({ + masterDerivationKey: MasterDerivationKey +}) + +export const ed25519PublicKey = z.instanceof(Uint8Array) + +export const PublicKey = ed25519PublicKey + +export const ExportMultisigResponse = z.object({ + multisigVersion: z.number().int(), + publicKeys: z.array(PublicKey), + threshold: z.number().int() +}) + +export const ImportMultisigResponse = z.object({ + address: z.instanceof(Address) +}) + +export const ListMultisigResponse = z.object({ + addresses: z.array(z.instanceof(Address)) +}) + +export const SignProgramMultisigResponse = z.object({ + multisig: z.instanceof(Uint8Array) +}) + +export const SignMultisigResponse = z.object({ + multisig: z.instanceof(Uint8Array) +}) + +export const SignProgramResponse = z.object({ + sig: z.instanceof(Uint8Array) +}) + +export const SignTransactionResponse = z.object({ + signedTransaction: z.instanceof(Uint8Array) +}) + +export const WalletHandle = z.object({ + expiresSeconds: z.number().int(), + wallet: Wallet +}) + +export const WalletInfoResponse = z.object({ + walletHandle: WalletHandle +}) + +export const InitWalletHandleTokenResponse = z.object({ + walletHandleToken: z.string() +}) + +export const RenameWalletResponse = z.object({ + wallet: Wallet +}) + +export const RenewWalletHandleTokenResponse = z.object({ + walletHandle: WalletHandle +}) + +export const CreateWalletResponse = z.object({ + wallet: Wallet +}) + +export const CreateWalletRequest = z.object({ + masterDerivationKey: MasterDerivationKey.optional(), + walletDriverName: z.string().optional(), + walletName: z.string(), + walletPassword: z.string() +}) + +export const DeleteKeyRequest = z.object({ + address: z.instanceof(Address), + walletHandleToken: z.string(), + walletPassword: z.string().optional() +}) + +export const DeleteMultisigRequest = z.object({ + address: z.instanceof(Address), + walletHandleToken: z.string(), + walletPassword: z.string().optional() +}) + +export const Digest = z.instanceof(Uint8Array) + +export const ExportKeyRequest = z.object({ + address: z.instanceof(Address), + walletHandleToken: z.string(), + walletPassword: z.string().optional() +}) + +export const ExportMasterKeyRequest = z.object({ + walletHandleToken: z.string(), + walletPassword: z.string().optional() +}) + +export const ExportMultisigRequest = z.object({ + address: z.instanceof(Address), + walletHandleToken: z.string() +}) + +export const GenerateKeyRequest = z.object({ + walletHandleToken: z.string() +}) + +export const ImportKeyRequest = z.object({ + privateKey: z.instanceof(Uint8Array), + walletHandleToken: z.string() +}) + +export const ImportMultisigRequest = z.object({ + multisigVersion: z.number().int(), + publicKeys: z.array(PublicKey), + threshold: z.number().int(), + walletHandleToken: z.string() +}) + +export const InitWalletHandleTokenRequest = z.object({ + walletId: z.string(), + walletPassword: z.string() +}) + +export const ListKeysRequest = z.object({ + walletHandleToken: z.string() +}) + +export const ListMultisigRequest = z.object({ + walletHandleToken: z.string() +}) + +export const ListWalletsRequest = z.record(z.string(), z.any()) + +export const ed25519Signature = z.instanceof(Uint8Array) + +export const Signature = ed25519Signature + +export const MultisigSubsig = z.object({ + publicKey: PublicKey, + signature: Signature.optional() +}) + +export const MultisigSig = z.object({ + subsignatures: z.array(MultisigSubsig), + threshold: z.number().int(), + version: z.number().int() +}) + +export const ed25519PrivateKey = z.instanceof(Uint8Array) + +export const PrivateKey = ed25519PrivateKey + +export const ReleaseWalletHandleTokenRequest = z.object({ + walletHandleToken: z.string() +}) + +export const RenameWalletRequest = z.object({ + walletId: z.string(), + walletName: z.string(), + walletPassword: z.string() +}) + +export const RenewWalletHandleTokenRequest = z.object({ + walletHandleToken: z.string() +}) + +export const SignMultisigTxnRequest = z.object({ + partialMultisig: MultisigSig.optional(), + publicKey: PublicKey, + signer: Digest.optional(), + transaction: z.instanceof(Uint8Array), + walletHandleToken: z.string(), + walletPassword: z.string().optional() +}) + +export const SignProgramMultisigRequest = z.object({ + address: z.instanceof(Address), + program: z.instanceof(Uint8Array), + partialMultisig: MultisigSig.optional(), + publicKey: PublicKey, + useLegacyMsig: z.boolean().optional(), + walletHandleToken: z.string(), + walletPassword: z.string().optional() +}) + +export const SignProgramRequest = z.object({ + address: z.instanceof(Address), + program: z.instanceof(Uint8Array), + walletHandleToken: z.string(), + walletPassword: z.string().optional() +}) + +export const SignTxnRequest = z.object({ + publicKey: PublicKey.optional(), + transaction: z.instanceof(Uint8Array), + walletHandleToken: z.string(), + walletPassword: z.string().optional() +}) + +export const VersionsRequest = z.record(z.string(), z.any()) + +export const VersionsResponse = z.object({ + versions: z.array(z.string()) +}) + +export const WalletInfoRequest = z.object({ + walletHandleToken: z.string() +}) diff --git a/packages/kmd_client/vitest.config.ts b/packages/kmd_client/vitest.config.ts index 6e7e01e2..340d7da5 100644 --- a/packages/kmd_client/vitest.config.ts +++ b/packages/kmd_client/vitest.config.ts @@ -5,7 +5,8 @@ export default defineConfig({ test: { include: ['tests/**/*.test.ts'], exclude: ['node_modules'], - globalSetup: ['./tests/globalSetup.ts'], + // Disabled: localnet tests don't need mock server setup + // globalSetup: ['./tests/globalSetup.ts'], testTimeout: 30_000, hookTimeout: 60_000, coverage: { diff --git a/scripts/generate-zod-schemas.ts b/scripts/generate-zod-schemas.ts index afc0ba2a..07b52cd5 100644 --- a/scripts/generate-zod-schemas.ts +++ b/scripts/generate-zod-schemas.ts @@ -514,6 +514,11 @@ function arrayToZod(schema: SchemaObject, bigintFields: Set, recursiveSc return 'z.array(z.any())' } + // Detect uint8 array (byte array) - should be Uint8Array + if (schema.items.type === 'integer' && schema.items.format === 'uint8') { + return 'z.instanceof(Uint8Array)' + } + const itemsZod = schemaToZod(schema.items, bigintFields, recursiveSchemas, strict) return `z.array(${itemsZod})` }