# Store & Details Pages

Canonical: https://docs.projectbabbage.com/docs/quickstarts/metamarket/store-details

Fetch and decode UTXO listings to browse files and complete secure purchases.

This page shows how to add browsing and purchasing functionality to your marketplace application. We'll be adding the functionality to:

**Backend**
- Add a `findStore` query to return active listings
- Add a `findDetails` query to return a single listing by `fileUrl`

**Key Server**
- Configure the payment middleware for `/purchase/:fileUrl`
- Implement the `/purchase/:fileUrl` route to update balances and return the decryption key

**Frontend**
- Wire up a **Store** page that calls `findStore` and decodes UTXO fields
- Wire up a **Details** page that calls and decodes `findDetails`, and submits a purchase to the key server

---

## 1. Backend: Store & Details Queries

### 1.1. Add `findStore` and `findByName` to MarketStorage

In `backend/src/lookup-services/MarketStorage.ts`, add these methods that check for and return active listings:

```ts
// backend/src/lookup-services/MarketStorage.ts

// … existing methods …

/**
 * Returns all active listings
 */
async findStore(): Promise<UTXOReference[]> {
  return await this.records.find({ retentionPeriod: { $gte: Math.floor(Date.now()) } })
    .project<UTXOReference>({ txid: 1, outputIndex: 1 })
    .toArray()
    .then(results => results.map(record => ({
      txid: record.txid,
      outputIndex: record.outputIndex
    })))
}

/**
 * Returns all listings by name
 */
async findByName(name: string): Promise<UTXOReference[]> {
  return await this.records.find({
    name: { $regex: name, $options: 'i' },
    retentionPeriod: {
      $gte: Math.floor(Date.now())
    }
  })
    .project<UTXOReference>({
      txid: 1,
      outputIndex: 1
    })
    .toArray()
    .then(results => results.map(record => ({
      txid: record.txid,
      outputIndex: record.outputIndex
    })))
}
```

**What’s happening:**

- For both of these methods, use a MongoDB `$gte` to compare the `retentionPeriod` of all of the stored UTXOs with the current time and return the `txid` and `outputIndex` of those listings.
- `findStore` returns all of the active listings stored in the database, while `findByName` returns the listings with the requested name.

---

### 1.2. Add `findDetails` to MarketLookupService

In `backend/src/lookup-services/MarketLookupServiceFactory.ts`, add the helper functions after the class and update the `lookup` method to handle both store and details queries:

```ts
// Query type checks

function isNameSearchQuery(query: any): query is {
  type: 'findByName', value: { name: string }
} {
  return (
    typeof query === 'object' &&
    query.type === 'findByName' &&
    query.value &&
    typeof query.value.name === 'string'
  )
}

function isDetailsQuery(query: any): query is {
  type: 'findDetails'; value: { txid: string, outputIndex: number }
} {
  return (
    typeof query === 'object' &&
    query.type === 'findDetails' &&
    query.value &&
    typeof query.value.txid === 'string' &&
    typeof query.value.outputIndex === 'number'
  )
}
```

```ts
// backend/src/lookup-services/MarketLookupServiceFactory.ts
export class MarketLookupService implements LookupService {
  readonly admissionMode: AdmissionMode = 'locking-script'
  readonly spendNotificationMode: SpendNotificationMode = 'none'

  constructor(public storage: MarketStorage) { }

  // … existing outputAdmittedByTopic and outputSpent …

  async lookup(question: LookupQuestion): Promise<LookupAnswer | LookupFormula> {
    // … existing lookup queries …
    
    // store → return all listings that are not expired
    if (query === 'findStore') {
      return await this.storage.findStore();
    }

    // store → return the listings that match the name and are not expired
    if (isNameSearchQuery(query)) {
      const { name } = query.value
      return await this.storage.findByName(name)
    }

    // details → returns the txid and outputIndex that will be formatted into BEEF
    if (isDetailsQuery(query)) {
      const { txid, outputIndex } = query.value
      console.log(txid, outputIndex)
      return [{ txid, outputIndex }]
    }

    throw new Error('Unknown query type')
  }

  // … existing getDocumentation & getMetaData …
}
```

**What’s happening:**

- **`findStore`** → when `query.type === 'findStore'`, we return the array of UTXO references for active listings.
- **`findDetails`** → when `query.type === 'findDetails'`, we return the `txid` and `outputIndex`, which the `lookupService` will verify is an unspent token and return the output's BEEF.

---

## 2. Frontend: Store Page

### 2.1. Update App.tsx to point “/” to Store

In `frontend/src/App.tsx`, change the default route to render `Store.tsx` instead of the placeholder `Home.tsx`:

```diff
// frontend/src/App.tsx
import Store from './pages/Store'

 function App() {
   return (
     <Layout>
       <Routes>
-        <Route path="/" element={<Home />} />
+        <Route path="/" element={<Store />} />
         <Route path="/upload" element={<UploadFile />} />
         <Route path="/details/:fileUrl" element={<Details />} />
         <Route path="/account" element={<Account />} />
       </Routes>
     </Layout>
   )
 }
```

---

### 2.2. Implement decodeOutputs.ts

Implement `frontend/src/utils/decodeOutputs.ts` to decode fields from each UTXO:

```ts
// frontend/src/utils/decodeOutputs.ts
import { Transaction, PushDrop, Utils } from '@bsv/sdk'

export interface DecodedOutput {
  fileUrl?: string
  name?: string
  description?: string
  satoshis?: number
  creatorPublicKey?: string
  size?: number
  txid: string
  outputIndex: number
  retentionPeriod?: number
  coverUrl?: string
}

type FieldKey = keyof DecodedOutput

export async function decodeOutput(
  beef: number[],
  outputIndex: number,
  fields: FieldKey[]
): Promise<DecodedOutput> {
  const decodedTx = Transaction.fromBEEF(beef)
  const outputs = decodedTx.outputs[outputIndex]
  const decoded = PushDrop.decode(outputs.lockingScript)
  const data = decoded.fields

  const record: DecodedOutput = {
    txid: decodedTx.id('hex'),
    outputIndex,
  }

  for (const key of fields) {
    switch (key) {
      case 'fileUrl':
        record.fileUrl = Utils.toUTF8(Utils.toArray(data[0]))
        break
      case 'name':
        record.name = Utils.toUTF8(Utils.toArray(data[1]))
        break
      case 'description':
        record.description = Utils.toUTF8(Utils.toArray(data[2]))
        break
      case 'satoshis':
        record.satoshis = parseInt(Utils.toUTF8(Utils.toArray(data[3])), 10)
        break
      case 'creatorPublicKey':
        record.creatorPublicKey = Utils.toUTF8(Utils.toArray(data[4]))
        break
      case 'size':
        record.size = parseInt(Utils.toUTF8(Utils.toArray(data[5])), 10)
        break
      case 'retentionPeriod':
        record.retentionPeriod = parseInt(
          Utils.toUTF8(Utils.toArray(data[6])),
          10
        )
        break
      case 'coverUrl':
        record.coverUrl = Utils.toUTF8(Utils.toArray(data[7]))
        break
      case 'txid':
      case 'outputIndex':
      default:
        break
    }
  }
  return record
}

export async function decodeOutputs(
  outputs: Array<{ beef: number[]; outputIndex: number }>,
  fields: FieldKey[]
): Promise<DecodedOutput[]> {
  return Promise.all(
    outputs.map(({ beef, outputIndex }) =>
      decodeOutput(beef, outputIndex, fields)
    )
  )
}
```

**What’s happening:**

- We loop over each UTXO’s `beef` and `outputIndex`, decode the PushDrop script, and then map the requested `fields` into a `DecodedOutput` object.
- For the store page, we only need the `{ name, satoshis, coverUrl, retentionPeriod }` fields and we can only request those to be extracted.

---

### 2.3. Add lookup query to Store.tsx

Implement the lookup and decoding of the output from the Overlay in the `fetchFiles` function in `frontend/src/pages/Store.tsx`:

```ts
// frontend/src/pages/Store.tsx

const fetchFiles = async (query?: string) => {
  setLoading(true)
  try {
    let response = null
    if (query && filter === 'name') {
      response = await lookupResolver.query({
        service: 'ls_demo_market',
        query: {
          type: 'findByName',
          value: { name: searchTerm }
        }
      })
    } else {
      response = await lookupResolver.query({
        service: 'ls_demo_market',
        query: 'findStore'
      })
    }
    if (response.type !== 'output-list') throw new Error('Lookup answer must be an output-list')

    const fileData = await decodeOutputs(response.outputs, fields)
    setFiles(fileData as StoreRecord[])
  } catch (error) {
    console.error('Error fetching files:', error)
  } finally {
    setLoading(false)
  }
}
```

**What’s happening:**

1. We create a `lookupResolver` query asking for the 
2. We decode each returned UTXO for the fields needed to render a card.
3.  The UI displays a grid of cards; each links to `/details/{txid}.{outputIndex}`.

---
## 3. Key Server: Purchase Route

### 3.1. Add payment middleware logic

Start by adding this query function to `key-server/src/KeyStorage.ts` that returns the details of a stored file:

```ts
async findByQuery(
  fileUrl: string
): Promise<KeyRecord[]> {
  try {
    return await this.records
      .find({ fileUrl })
      .toArray();
  } catch (error) {
    console.error('Failed to find records by query:', error);
    throw error;
  }
}
```

Now in `key-server/src/index.ts`, we want to configure the payment middleware to process a transaction when the `/purchase` route is accessed:

```ts
// key-server/src/index.ts
import { createAuthMiddleware } from '@bsv/auth-express-middleware'
import { createPaymentMiddleware } from '@bsv/payment-express-middleware'
import { getWallet } from './utils/walletSingleton.js'

// … after getWallet() …

app.use(createAuthMiddleware({
  wallet,
  allowUnauthenticated: false,
  logger: console,
  logLevel: 'error'
}))

app.use(createPaymentMiddleware({
  wallet,
  calculateRequestPrice: async (req) => {
    // only request a payment if the url includes /purchase
    if (!req.url.includes('/purchase')) {
      return 0
    }
    const { fileUrl } = (req.body as any) || {}
    try {
      if (!fileUrl) return 0
      const record = await keyStorage.findByQuery(fileUrl)
      if (!record || record.length != 1) {
        return 0
      }
      console.log(record[0].satoshis)
      return record[0].satoshis
    } catch (e) {
      return 0
    }
  }
}))
```

**What’s happening:**

- All incoming requests pass through an auth check via `AuthMiddleware`, requiring `AuthFetch` and a connected BRC-100 wallet to access.
- `PaymentMiddleware` is now configured to charge on `/purchase/:fileUrl`, parsing the `fileUrl` from the body and querying the key server database for the price of the file.
- The `PaymentMiddleware` then charges the purchaser of the file with the cost and stores the satoshis on the key server.

---

### 3.2 Add balance collection to the database

In `key-server/src/KeyStorage.ts`, create a new MongoDB collection to store and to increment the file uploader's balance when anyone buys their files so that they can later withdraw and receive the satoshis:

```
// key-server/src/types.ts

export interface BalanceRecord {
  publicKey: string
  balance: number
}
```

```ts
// key-server/src/KeyStorage.ts

import dotenv from 'dotenv'
import { Collection, Db } from 'mongodb'
import { BalanceRecord, KeyRecord } from './types.js'

dotenv.config()

const KEY_COLLECTION_NAME = process.env.KEY_COLLECTION_NAME as string
const BALANCE_COLLECTION_NAME = process.env.BALANCE_COLLECTION_NAME as string

export class KeyStorage {
  private readonly records: Collection<KeyRecord>
  private readonly balances: Collection<BalanceRecord>

  constructor(private readonly db: Db) {
    this.records = db.collection<KeyRecord>(KEY_COLLECTION_NAME)
    this.balances = db.collection<BalanceRecord>(BALANCE_COLLECTION_NAME)
  }
  // …
```

Also add the `incrementBalance` method that increments the given public key's balance, creating a new entry in the collection if one does not already exist:

```ts
async incrementBalance(
  publicKey: string, amount: number
): Promise<void> {
  if (amount <= 0) return

  await this.balances.updateOne(
    { publicKey },
    { $inc: { balance: amount } },
    { upsert: true }
  )
}
```

---

### 3.3. Implement /purchase

After the payment middleware processes the transaction and stores the purchaser's satoshis on the server, the `/purchase/:fileUrl` route will be accessed and will need 

In `key-server/src/index.ts`:

```ts
// key-server/src/index.ts (add after /submit)

app.post('/purchase/:fileUrl', async (req: Request, res: Response): Promise<any> => {
  try {
    const { fileUrl } = req.body
    if (!fileUrl) return res.status(400).json({ error: 'No fileUrl provided' })
    
    // lookup matching record
    const record = await keyStorage.findByQuery(fileUrl)
    if (!record || record.length != 1) {
      return res.status(404).json({ error: 'File not found on key storage' })
    }
    
    // increment uploader’s balance
    const { satoshis, publicKey } = record[0]
    await keyStorage.incrementBalance(publicKey, satoshis)

    // return decryption key to purchaser
    const encryptionKey = record[0].encryptionKey
    return res.status(200).json({
      success: true,
      message: `Uploader balance updated. +${satoshis} sats for ${publicKey}`,
      encryptionKey
    })
  } catch (error) {
    console.error('Error processing purchase:', error)
    return res.status(500).json({ error: (error as Error).message })
  }
})
```

**What’s happening:**

1. We read `fileUrl` from the URL parameter.
2. We query `files` collection via `findByQuery(fileUrl)` (returns the matching record).
3. We call `incrementBalance(publicKey, satoshis)` to credit the uploader.
4. We return `{ encryptionKey }` so the buyer can decrypt the STL file.


---
## 4. Frontend: Details Page

### 4.1. Add lookup query to Details.tsx



First we need to add the query and parse the information of the requisite file. We are getting the txid and outputIndex from the URL of the page:

```ts
// frontend/src/pages/Details.tsx

const fetchDetails = async () => {
  try {
    const response = await lookupResolver.query({
      service: 'ls_demo_market',
      query: {
        type: 'findDetails',
        value: {
          txid,
          outputIndex: parseInt(outputIndex || '0', 10)
        }
      }
    })
    if (response.type !== 'output-list') throw new Error('Lookup answer must be an output-list')

    const fileData = await decodeOutputs(response.outputs, fields)
    setDetails(fileData[0] as DetailsRecord)
  } catch (error) {
    throw new Error(`Error fetching details: ${error}`)
  } finally {
    setIsLoading(false)
  }
}
```

Next, we want to implement the purchasing and decryption of the file:

```ts
// frontend/src/pages/Details.tsx

const handlePurchase = async () => {
  try {
    if (!details) return
    setIsLoading(true)

    const fileUrl = details.fileUrl
    if (!fileUrl) {
      throw new Error('No fileUrl available to purchase!')
    }

    console.log('File url:', fileUrl)

    const wallet = new WalletClient('auto')
    const authFetch = new AuthFetch(wallet)

    const keyUrl = `${constants.keyServer}/purchase/${fileUrl}`

    // hit the /purchase endpoint and trigger the payment middleware
    let payResponse
    try {
      payResponse = await authFetch.fetch(
        keyUrl,
        {
          method: 'POST',
          body: JSON.stringify({ fileUrl }),
          headers: {
            'Content-Type': 'application/json'
          }
        }
      )
    } catch (error) {
      throw new Error(`Failed to complete purchase: ${error}`)
    }
    if (!payResponse) throw new Error('Failed to complete purchase')

    const purchaseResult = await payResponse.json()
    const encryptionKey = purchaseResult.encryptionKey
    if (!encryptionKey) {
      if (!purchaseResult.description) {
        throw new Error('Purchase was successful, but no encryptionKey was returned.')
      }
      else {
        throw new Error(purchaseResult.description)
      }
    }

    const storageDownloader = new StorageDownloader()
    const { data: encryptedBytes, mimeType } = await storageDownloader.download(fileUrl)
    console.log('Downloaded file from UHRP, mimeType:', mimeType)

    // decrypting the file
    const symmetricKey = new SymmetricKey(encryptionKey, 'hex')
    const decryptedBytes = symmetricKey.decrypt(encryptedBytes) as number[] // test

    const blob = new Blob(
      [Uint8Array.from(decryptedBytes)],
      { type: 'model/stl' })
    const url = URL.createObjectURL(blob)
    setDecryptedFileURL(url)
  } catch (error) {
    console.error('Error during purchase:', error)
  } finally {
    setIsLoading(false)
  }
}
```


**What’s happening:**

1. On purchase, call the key server’s `/purchase/:fileUrl` endpoint, which causes the payment middleware to facilitate the transaction. 
2. The key server then credits the file uploader’s balance and responds with the `encryptionKey` on success.
3. The frontend then decrypts the file using the key and allows the purchaser to download the STL file.

---

## 5. Summary

With these changes in place, your marketplace now supports browsing active listings, viewing detailed file information, and securely purchasing and decrypting 3D-printable files.

```
my-marketplace/
├── backend/
│   ├── src/
│   │   ├── lookup-services/
│   │   │   ├── MarketLookupServiceFactory.ts
│   │   │   └── MarketStorage.ts
│   │   ├── topic-managers/
│   │   │   └── MarketTopicManager.ts
│   │   ├── types.ts
│   │   └── … other backend files …
│   └── package.json
├── frontend/
│   ├── src/
│   │   ├── pages/
│   │   │   ├── Store.tsx
│   │   │   └── Details.tsx
│   │   ├── utils/
│   │   │   ├── decodeOutputs.ts
│   │   │   └── publishCommitment.ts
│   │   ├── App.tsx
│   │   └── … other frontend files …
│   └── package.json
├── key-server/
│   ├── src/
│   │   ├── index.ts       ← includes /purchase/:fileUrl
│   │   ├── KeyStorage.ts
│   │   ├── types.ts
│   │   └── utils/
│   │       └── decodeOutputs.ts
│   └── package.json
├── deployment-info.json
└── package.json
```

**Backend:**

  - `findStore` returns active listing references.
  - `findDetails` returns a reference for a given `fileUrl`.

**Frontend:**

  - **Store.tsx** displays active listings by decoding fields: `{ name, description, satoshis, coverUrl }`.
  - **Details.tsx** shows listing details, decodes `{ fileUrl, name, description, satoshis, creatorPublicKey, size, retentionPeriod, coverUrl }`, and calls `/purchase/:fileUrl`.

**Key Server:**

  - **`/purchase/:fileUrl`** credits the uploader’s balance and returns the `encryptionKey` to the buyer.

---

## 6. Verify in the UI

Once you’ve uploaded a file and completed the purchase flow, your **Store** and **Details** pages will render real data from your Overlay and Key Server.

### Store Page

![Store Page with Active Listings](https://docs.projectbabbage.com/assets/images/metamarket-store.webp)
*Browse all active 3D-printable files in the marketplace*

### Details Page

![Details Page with Purchase Button](https://docs.projectbabbage.com/assets/images/metamarket-details.webp)
*View file metadata, purchase, and download the decrypted STL*

With these changes, readers can browse all of the listed files, view the details, and purchase to receive the decrypted file to download!
