Key Server Uploading
Verify on-chain commitments and persist encryption keys and payment data in your key server.
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-parserUpdate 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},This replaces the ts-node-dev script added earlier.
1{
2- "type": "commonjs",
3+ "type": "module",
4}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 CARSgenerate 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}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}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}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}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})What's happening:
- Load environment variables
- Parse JSON bodies & enable CORS
- Apply authentication & payment guards
- 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})Key steps in /submit:
- Validate request body & UHRP URL
- Confirm admission on-chain via Lookup Service
- 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 startYou should see logs similar to:
1Server is running on port 3000
2Connected to MongoDBIf 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!
