Skip to main content

Overview

A KeyService is responsible for managing cryptographic key pairs and performing signing operations. It acts as a secure abstraction layer between your application logic and the actual private keys.

MemoryKeyService

The MemoryKeyService is the default implementation that stores key pairs in memory. It’s suitable for server-side applications, CLI tools, and testing.

Type Definition

type MemoryKeyService = {
  // Throwable variants
  signTransaction: SignTransaction;
  findKeyPair: FindKeyPair;
  
  // Safe variants
  safeSignTransaction: SafeSignTransaction;
  safeFindKeyPair: SafeFindKeyPair;
};

Internal Context

type MemoryKeyServiceContext = {
  keyPairs: Record<PublicKey, KeyPair>;  // Map of public keys to key pairs
  safeFindKeyPair: SafeFindKeyPair;      // Safe key lookup function
};

Creating a MemoryKeyService

Single Key Source

import { createMemoryKeyService } from '@near-api/client';

const keyService = createMemoryKeyService({
  keySource: {
    privateKey: 'ed25519:3D4YudUahN1...'
  }
});

Multiple Key Sources

const keyService = createMemoryKeyService({
  keySources: [
    { privateKey: 'ed25519:3D4YudUahN1...' },
    { privateKey: 'ed25519:5B2KvwRnG2...' },
    { privateKey: 'secp256k1:4F8HxmPqR3...' }
  ]
});
The service automatically derives the public key from each private key and indexes them for fast lookup.

Safe Creation

import { safeCreateMemoryKeyService } from '@near-api/client';

const result = safeCreateMemoryKeyService({
  keySources: [
    { privateKey: 'ed25519:...' }
  ]
});

if (result.ok) {
  const keyService = result.value;
} else {
  console.error(result.error.kind);
  // 'CreateMemoryKeyService.Args.InvalidSchema'
  // 'CreateMemoryKeyService.Internal'
}

Key Service Methods

signTransaction

Sign a transaction with the appropriate private key:
const signedTx = await keyService.signTransaction({
  transaction: {
    signerAccountId: 'alice.near',
    signerPublicKey: 'ed25519:...',
    receiverAccountId: 'bob.near',
    nonce: 12345,
    blockHash: '...',
    actions: [transferAction]
  }
});

console.log(signedTx.signature);
console.log(signedTx.transactionHash);
try {
  const signed = await keyService.signTransaction({
    transaction: myTransaction
  });
  // Use signed transaction
} catch (error) {
  // Handle error
}

findKeyPair

Look up a key pair by its public key:
const keyPair = await keyService.findKeyPair({
  publicKey: 'ed25519:...'
});

console.log(keyPair.publicKey);
console.log(keyPair.privateKey);
Safe variant:
const result = await keyService.safeFindKeyPair({
  publicKey: 'ed25519:...'
});

if (result.ok) {
  const keyPair = result.value;
} else {
  // 'MemoryKeyService.FindKeyPair.NotFound'
  console.log('Key not found:', result.error.context.publicKey);
}

Key Pair Management

Generating Key Pairs

The library provides utilities to generate new key pairs:
import { randomEd25519KeyPair, randomSecp256k1KeyPair } from '@near-api/client';

// Generate Ed25519 key pair (recommended)
const ed25519KeyPair = randomEd25519KeyPair();
console.log(ed25519KeyPair.publicKey);   // "ed25519:..."
console.log(ed25519KeyPair.privateKey);  // "ed25519:..."

// Generate Secp256k1 key pair
const secp256k1KeyPair = randomSecp256k1KeyPair();

Creating Key Pairs from Existing Keys

import { keyPair } from '@near-api/client';

const kp = keyPair({
  curve: 'ed25519',
  privateKey: 'ed25519:3D4YudUahN1...'
});
Safe variant:
import { safeKeyPair } from '@near-api/client';

const result = safeKeyPair({
  curve: 'ed25519',
  privateKey: 'ed25519:...'
});

if (!result.ok) {
  // 'CreateKeyPair.Args.InvalidSchema'
  // 'CreateKeyPair.Internal'
}

KeyPair Type

type KeyPair = {
  publicKey: PublicKey;   // "ed25519:..." or "secp256k1:..."
  privateKey: PrivateKey; // "ed25519:..." or "secp256k1:..."
  curve: 'ed25519' | 'secp256k1';
};

Supported Curves

The NEAR API TypeScript library supports two elliptic curves:

Ed25519

Recommended for most use cases
  • Default choice for NEAR
  • Faster signing and verification
  • Smaller signatures (64 bytes)
  • Better security properties

Secp256k1

For Bitcoin/Ethereum compatibility
  • Used in Bitcoin and Ethereum
  • Larger signatures
  • Required for cross-chain applications

Error Handling

The MemoryKeyService can produce the following errors:
interface MemoryKeyServicePublicErrorRegistry {
  'CreateMemoryKeyService.Args.InvalidSchema': InvalidSchemaErrorContext;
  'CreateMemoryKeyService.Internal': InternalErrorContext;
  
  'MemoryKeyService.SignTransaction.Args.InvalidSchema': InvalidSchemaErrorContext;
  'MemoryKeyService.SignTransaction.SigningKeyPair.NotFound': {
    signerPublicKey: PublicKey;
  };
  'MemoryKeyService.SignTransaction.Internal': InternalErrorContext;
  
  'MemoryKeyService.FindKeyPair.Args.InvalidSchema': InvalidSchemaErrorContext;
  'MemoryKeyService.FindKeyPair.NotFound': { 
    publicKey: PublicKey 
  };
  'MemoryKeyService.FindKeyPair.Internal': InternalErrorContext;
}

Common Error Scenarios

const result = await keyService.safeSignTransaction({
  transaction: {
    signerPublicKey: 'ed25519:WrongKey...',
    // ... other fields
  }
});

if (!result.ok && 
    result.error.kind === 'MemoryKeyService.SignTransaction.SigningKeyPair.NotFound') {
  console.log('The keyService does not have this key:', 
              result.error.context.signerPublicKey);
  // Add the key to the service or use a different public key
}

Integration with Signers

KeyServices are primarily used by Signers to perform cryptographic operations:
import { createClient, createMemoryKeyService, createMemorySigner } from '@near-api/client';

// 1. Create client
const client = createClient({
  transport: {
    rpcEndpoints: {
      archival: [{ url: 'https://rpc.testnet.near.org' }]
    }
  }
});

// 2. Create key service with your keys
const keyService = createMemoryKeyService({
  keySources: [
    { privateKey: 'ed25519:...' }
  ]
});

// 3. Create signer that uses the key service
const signer = createMemorySigner({
  signerAccountId: 'alice.near',
  client,
  keyService
});

// Now the signer will use keyService to sign transactions
await signer.executeTransaction({
  intent: {
    receiverAccountId: 'bob.near',
    action: transferAction
  }
});

Security Considerations

Memory Storage

MemoryKeyService stores keys in memory, which means:
  • Keys are lost when the process terminates
  • Keys are vulnerable if the process memory is compromised
  • Not suitable for long-term key storage in production web applications

Best Practices

  • Server-side: Use MemoryKeyService with keys loaded from secure storage
  • Client-side: Consider implementing a custom KeyService that integrates with browser wallet extensions
  • Testing: MemoryKeyService is perfect for unit tests and integration tests
  • Key rotation: Regularly rotate keys and use function call keys when possible

Custom Key Services

You can implement your own KeyService to integrate with hardware wallets, KMS, or browser wallets:
interface CustomKeyService {
  signTransaction: (args: { transaction: Transaction }) => Promise<SignedTransaction>;
  safeSignTransaction: (args: { transaction: Transaction }) => Promise<Result<SignedTransaction, Error>>;
  findKeyPair: (args: { publicKey: PublicKey }) => Promise<KeyPair>;
  safeFindKeyPair: (args: { publicKey: PublicKey }) => Promise<Result<KeyPair, Error>>;
}

const customKeyService: CustomKeyService = {
  signTransaction: async ({ transaction }) => {
    // Integrate with your hardware wallet or KMS
    const signature = await myHardwareWallet.sign(transaction);
    return {
      transaction,
      signature,
      transactionHash: computeHash(transaction)
    };
  },
  // ... implement other methods
};