Key Server Uploading

Verify on-chain commitments and persist encryption keys and payment data in your key server.

Read as Markdown

This page walks through setting up your Key Server to handle file‐upload submissions for the Metanet 3D Marketplace: installing dependencies, wiring environment variables, configuring wallet access, middleware, decoding on-chain payloads, and persisting metadata.


1. Install dependencies & configure .env

In your key-server/ directory, install required packages:

1cd key-server
2npm install express body-parser dotenv @bsv/sdk @bsv/wallet-toolbox \
3  @bsv/auth-express-middleware @bsv/payment-express-middleware mongodb prettyjson
4npm install --save-dev typescript ts-node-dev @types/node @types/express @types/body-parser
Copied!

Update the type and your start script in key-server/package.json:

1"scripts": {
2- "start": "ts-node-dev --respawn src/index.ts"
3+ "start": "node --loader ts-node/esm/transpile-only --max-http-header-size=10485760 src/index.ts"
4},
Copied!

This replaces the ts-node-dev script added earlier.

1{
2- "type": "commonjs",
3+ "type": "module",
4}
Copied!

Create a .env file in key-server/ with:

1PORT=3000
2SERVER_PRIVATE_KEY=your-server-private-key
3SERVER_URL=http://localhost:3000/
4WALLET_STORAGE_URL=https://storage.babbage.systems
5
6## MongoDB Configuration
7MONGO_URI=mongodb://localhost:27017
8DATABASE_NAME=your-database-name
9KEY_COLLECTION_NAME=files
10BALANCE_COLLECTION_NAME=balance
11
12BSV_NETWORK=mainnet # or testnet
13NETWORK_PRESET=local # or mainnet/testnet for deployment with CARS
Copied!

generate a unique SHA-256 hash as your private key and rename the database


2. Define Key Server types

Add key-server/src/types.ts:

1// key-server/src/types.ts
2export interface DecodedOutput {
3  fileUrl?: string
4  name?: string
5  description?: string
6  satoshis?: number
7  creatorPublicKey?: string
8  size?: number
9  txid: string
10  outputIndex: number
11  retentionPeriod?: number
12  coverUrl?: string
13}
14
15export interface KeyRecord {
16  fileUrl: string
17  encryptionKey: string
18  satoshis: number
19  publicKey: string
20}
Copied!

3. Create the getWallet util

We'll start by creating some helper utility functions and classes that we will later utilize in index.ts

Provide your server with a BSV wallet using @bsv/wallet-toolbox, this wallet will need to be funded at WUI using the private key you generated in the .env file:

1// key-server/src/utils/walletSingleton.ts
2import { KeyDeriver, PrivateKey, WalletInterface } from '@bsv/sdk'
3import { Services, StorageClient, Wallet, WalletSigner, WalletStorageManager } from '@bsv/wallet-toolbox';
4
5const walletInstance: WalletInterface | null = null
6
7export async function getWallet(): Promise<WalletInterface> {
8  const SERVER_PRIVATE_KEY = process.env.SERVER_PRIVATE_KEY as string
9  const WALLET_STORAGE_URL = process.env.WALLET_STORAGE_URL as string
10  const BSV_NETWORK = process.env.BSV_NETWORK as 'mainnet' | 'testnet'
11
12  if (!walletInstance) {
13    const chain = BSV_NETWORK === 'mainnet' ? 'main' : 'test'
14    const keyDeriver = new KeyDeriver(new PrivateKey(SERVER_PRIVATE_KEY, 'hex'));
15    const storageManager = new WalletStorageManager(keyDeriver.identityKey);
16    const signer = new WalletSigner(chain, keyDeriver, storageManager);
17    const services = new Services(chain);
18    const wallet = new Wallet(signer, services);
19    const client = new StorageClient(wallet, WALLET_STORAGE_URL);
20    await client.makeAvailable();
21    await storageManager.addWalletStorageProvider(client);
22    return wallet;
23  }
24  return walletInstance
25}
Copied!

What's happening:

  • Deriving the server identity key and setting it as the server's wallet's key
  • Wire up on-chain signer and storage client
  • Cache a single wallet instance that will be used throughout the project

4. Implement KeyStorage

Create key-server/src/KeyStorage.ts to persist file records:

1// key-server/src/KeyStorage.ts
2import { Collection, Db, DeleteResult } from 'mongodb';
3import { KeyRecord } from './types.js';
4import dotenv from 'dotenv'
5
6dotenv.config()
7
8const KEY_COLLECTION_NAME = process.env.KEY_COLLECTION_NAME as string
9
10export class KeyStorage {
11  private readonly records: Collection<KeyRecord>
12
13  constructor(private readonly db: Db) {
14    this.records = db.collection<KeyRecord>(KEY_COLLECTION_NAME)
15  }
16
17  async storeRecord(
18    fileUrl: string,
19    encryptionKey: string,
20    satoshis: number,
21    publicKey: string
22  ): Promise<void> {
23    console.log('Storing record in MongoDB:', {
24      fileUrl,
25      encryptionKey,
26      satoshis,
27      publicKey
28    });
29
30    try {
31      await this.records.insertOne({
32        fileUrl,
33        encryptionKey,
34        satoshis,
35        publicKey
36      });
37    } catch (error) {
38      console.error('Failed to store record:', error);
39      throw error;
40    }
41  }
42}
Copied!

5. Create the decodeOutputs util

This helper parses the BEEF payloads retrieved from your Topic Manager. Note that a very similar function will need to be set up for the frontend:

1// key-server/src/utils/decodeOutputs.ts
2import { Transaction, PushDrop, Utils } from '@bsv/sdk'
3import { DecodedOutput } from 'src/types.js'
4
5type FieldKey = keyof DecodedOutput
6
7export async function decodeOutput(
8  beef: number[],
9  outputIndex: number,
10  fields: FieldKey[]
11): Promise<DecodedOutput> {
12  const decodedTx = Transaction.fromBEEF(beef)
13  const outputs = decodedTx.outputs[outputIndex]
14  const decoded = PushDrop.decode(outputs.lockingScript)
15  const data = decoded.fields
16
17  const record: DecodedOutput = {
18    txid: decodedTx.id('hex'),
19    outputIndex,
20  }
21  
22  // loop through each of the keys and add the ones that match the requisite fields
23  for (const key of fields) {
24    switch (key) {
25      case 'fileUrl':
26        record.fileUrl = Utils.toUTF8(Utils.toArray(data[0]))
27        break
28      case 'name':
29        record.name = Utils.toUTF8(Utils.toArray(data[1]))
30        break
31      case 'description':
32        record.description = Utils.toUTF8(Utils.toArray(data[2]))
33        break
34      case 'satoshis':
35        record.satoshis = parseInt(data[3].toString(), 10)
36        break
37      case 'creatorPublicKey':
38        record.creatorPublicKey = Utils.toUTF8(Utils.toArray(data[4]))
39        break
40      case 'size':
41        record.size = parseInt(data[5].toString(), 10)
42        break
43      case 'retentionPeriod':
44        record.retentionPeriod = parseInt(
45          Utils.toUTF8(Utils.toArray(data[6])),
46          10
47        )
48        break
49      case 'coverUrl':
50        record.coverUrl = Utils.toUTF8(Utils.toArray(data[7]))
51        break
52      case 'txid':
53      case 'outputIndex':
54      default:
55        break
56    }
57  }
58  return record
59}
60
61export async function decodeOutputs(
62  outputs: Array<{ beef: number[]; outputIndex: number }>,
63  fields: FieldKey[]
64): Promise<DecodedOutput[]> {
65  return Promise.all(
66    outputs.map(({ beef, outputIndex }) =>
67      decodeOutput(beef, outputIndex, fields)
68    )
69  )
70}
Copied!

6. Configure Express & middleware

In key-server/src/index.ts, set up your app skeleton:

1// key-server/src/index.ts
2import express, { Express, Request, Response, NextFunction } from 'express'
3import bodyParser from 'body-parser'
4import dotenv from 'dotenv'
5import { createAuthMiddleware } from '@bsv/auth-express-middleware'
6import { createPaymentMiddleware } from '@bsv/payment-express-middleware'
7import { getWallet } from './utils/walletSingleton.js'
8import { MongoClient } from 'mongodb'
9import { KeyStorage } from './KeyStorage.js'
10import { LookupResolver, P2PKH, PrivateKey, PublicKey, StorageDownloader, StorageUtils, SymmetricKey, Utils } from '@bsv/sdk'
11import { decodeOutputs } from './utils/decodeOutputs.js'
12import { DecodedOutput } from './types.js'
13import { randomBytes } from 'crypto'
14
15(global.self as any) = { crypto }
16
17dotenv.config()
18
19const app: Express = express()
20const PORT = process.env.PORT || 3000
21const MONGO_URI = process.env.MONGO_URI as string
22const DATABASE_NAME = process.env.DATABASE_NAME as string
23const SERVER_PRIVATE_KEY = process.env.SERVER_PRIVATE_KEY as string
24const NETWORK_PRESET = process.env.NETWORK_PRESET as 'local' | 'mainnet' | 'testnet'
25
26// Core middleware
27app.use(bodyParser.json())
28app.use((req, res, next: NextFunction) => {
29  res.header('Access-Control-Allow-Origin', '*')
30  next()
31})
32
33// CORS Headers
34app.use((req, res, next: NextFunction) => {
35  res.header('Access-Control-Allow-Origin', '*')
36  res.header('Access-Control-Allow-Headers', '*')
37  res.header('Access-Control-Allow-Methods', '*')
38  res.header('Access-Control-Expose-Headers', '*')
39  res.header('Access-Control-Allow-Private-Network', 'true')
40  if (req.method === 'OPTIONS') {
41    res.sendStatus(200)
42  } else {
43    next()
44  }
45})
46
47const wallet = await getWallet()
48
49// Auth middleware: enforces authenticated identityKey
50app.use(createAuthMiddleware({
51  wallet,
52  allowUnauthenticated: false,
53  logger: console,
54  logLevel: undefined
55}))
56
57// Payment middleware: all endpoints free for now
58app.use(createPaymentMiddleware({
59  wallet,
60  calculateRequestPrice: async () => 0
61}))
62
63// MongoDB and KeyStorage setup
64let dbClient: MongoClient
65let keyStorage: KeyStorage
66
67
68app.listen(PORT, async () => {
69  console.log(`Server is running on port ${PORT}`)
70
71  try {
72    dbClient = new MongoClient(MONGO_URI)
73    await dbClient.connect()
74    console.log('Connected to MongoDB')
75
76    const db = dbClient.db(DATABASE_NAME)
77    keyStorage = new KeyStorage(db)
78  } catch (error) {
79    console.error('Failed to connect to MongoDB:', error)
80    process.exit(1)
81  }
82})
Copied!

What's happening:

  1. Load environment variables
  2. Parse JSON bodies & enable CORS
  3. Apply authentication & payment guards
  4. Connect to MongoDB and initialize KeyStorage

7. Add the /submit route

Finally, handle uploads by validating, decoding, and storing metadata:

1// key-server/src/index.ts (add after storage setup)
2
3app.post('/submit', async (req: Request, res: Response): Promise<any> => {
4  try {
5    const { fileUrl, encryptionKey, satoshis, publicKey } = req.body;
6
7    if (!fileUrl || !encryptionKey || !satoshis || !publicKey) {
8      return res.status(400).json({ message: 'Missing required fields' })
9    }
10
11    console.log(`File URL: ${fileUrl}`)
12    // checking if the file is on UHRP
13    if (!StorageUtils.isValidURL(fileUrl)) {
14      return res.status(400).json({ message: `Invalid file Url: ${fileUrl}` });
15    }
16
17    const storageDownloader = new StorageDownloader()
18
19    let resolvedUrl
20    for (let attempt = 1; attempt <= 6; attempt++) {
21      try {
22        resolvedUrl = await storageDownloader.resolve(fileUrl)
23        if (resolvedUrl) {
24          break
25        }
26      } catch (error) {
27        console.log(`Download attempt ${attempt} failed:`, error)
28        if (attempt < 12) {
29          await new Promise((resolve) => setTimeout(resolve, 5000))
30        } else {
31          throw new Error(`Download failed after ${attempt} attempts`)
32        }
33      }
34    }
35
36    if (!resolvedUrl || resolvedUrl.length === 0) {
37      return res.status(404).json({ message: 'File not found on UHRP', fileUrl });
38    }
39
40    // checking the encryption
41    const uhrpFile = await storageDownloader.download(fileUrl)
42    const encryptedDataArray = uhrpFile.data
43    if (!encryptedDataArray || encryptedDataArray.length === 0) {
44      return res.status(400).json({ message: 'Downloaded file is empty' })
45    }
46
47    try {
48      // ensure that the file decrypts
49      const symmetricKey = new SymmetricKey(encryptionKey, 'hex')
50      const decryptedFile = symmetricKey.decrypt(encryptedDataArray)
51      console.log('File decrypted successfully, length:', decryptedFile.length)
52    } catch (error) {
53      console.error('Decryption error:', error)
54      return res.status(400).json({ message: 'Failed to decrypt file' })
55    }
56
57
58    // checking if the file is properly stored in the backend server
59    for (let attempt = 1; attempt <= 6; attempt++) {
60      const lookupResolver = new LookupResolver({ networkPreset: NETWORK_PRESET })
61      try {
62        const check = await lookupResolver.query({
63          service: 'ls_demo_market',
64          query: {
65            type: 'urlCheck',
66            value: { fileUrl }
67          }
68        })
69
70        if (check.type !== 'output-list') throw new Error('Lookup answer must be an output-list')
71
72        const fields: (keyof DecodedOutput)[] = [
73          'txid',
74          'outputIndex'
75        ]
76        const result = await decodeOutputs(check.outputs, fields)
77
78        if (result.length >= 0) {
79          console.log(`Lookup success in ${attempt} attempts:`, result)
80          break
81        }
82      } catch (error) {
83        console.log(`Lookup attempt ${attempt} failed`)
84        break
85      }
86
87      if (attempt < 6) {
88        await new Promise((resolve) => setTimeout(resolve, 5000))
89      } else {
90        throw new Error(`Lookup failed after ${attempt} attempts`)
91      }
92    }
93
94    // finally store the file url and its key in the database
95    await keyStorage.storeRecord(fileUrl, encryptionKey, satoshis, publicKey)
96
97    return res.status(200).json({ message: 'File metadata saved successfully' })
98  } catch (error) {
99    console.error('Error submitting file data:', error)
100    res.status(500).json({ message: 'Failed to save file metadata' })
101  }
102})
Copied!

Key steps in /submit:

  1. Validate request body & UHRP URL
  2. Confirm admission on-chain via Lookup Service
  3. Store record in MongoDB

8. Run the Key Server

From your key-server/ directory, start the server and confirm it boots without errors:

1cd key-server
2npm run start
Copied!

You should see logs similar to:

1Server is running on port 3000
2Connected to MongoDB
Copied!

If those messages appear and no errors are thrown, your Key Server is correctly wired and ready to accept /submit requests from the frontend.

You've now fully configured your Key Server to receive upload submissions, verify on-chain commitment, and persist file metadata. Next: build the Upload Files page on your frontend to call this endpoint!

Was this page helpful?