Overlay Services
Last updated
mkdir my-token-overlay && cd my-token-overlay
npm init -y
npm install @bsv/sdk @bsv/overlay-express mongodb express dotenv
npm install -D typescript @types/node @types/expressPORT=3000
MONGODB_URI=mongodb://localhost:27017{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
}
}import { TopicManager, AdmittanceInstructions } from '@bsv/overlay-tools'
import { Transaction, PushDrop, Utils, OP } from '@bsv/sdk'
export class MyTokenTopicManager implements TopicManager {
async identifyAdmissibleOutputs(beef: number[]): Promise<AdmittanceInstructions> {
try {
const tx = Transaction.fromBEEF(beef)
const txid = tx.id('hex')
// Track balances: Map<tokenId, { amount: number, isMint: boolean }>
const balances = new Map<string, { amount: number, isMint: boolean }>()
// Process inputs (credits)
for (const input of tx.inputs) {
if (!input.sourceTransaction) continue
try {
const sourceOutput = input.sourceTransaction.outputs[input.sourceOutputIndex]
const token = PushDrop.decode(sourceOutput.lockingScript)
let tokenId = Utils.toUTF8(token.fields[0])
if (tokenId === '___mint___') {
tokenId = `${input.sourceTransaction.id('hex')}.${input.sourceOutputIndex}`
}
const amount = Number(new Utils.Reader(token.fields[1]).readUInt64LEBn())
const current = balances.get(tokenId) || { amount: 0, isMint: false }
balances.set(tokenId, { amount: current.amount + amount, isMint: current.isMint })
} catch { continue }
}
// Process outputs (debits)
const outputsToAdmit: number[] = []
for (let vout = 0; vout < tx.outputs.length; vout++) {
const output = tx.outputs[vout]
const chunks = output.lockingScript.chunks
if (chunks.length < 2 || chunks[chunks.length - 1].op !== OP.OP_CHECKSIG) continue
try {
const token = PushDrop.decode(output.lockingScript)
let tokenId = Utils.toUTF8(token.fields[0])
if (tokenId === '___mint___') {
tokenId = `${txid}.${vout}`
balances.set(tokenId, { amount: 0, isMint: true })
}
const amount = Number(new Utils.Reader(token.fields[1]).readUInt64LEBn())
const current = balances.get(tokenId) || { amount: 0, isMint: false }
balances.set(tokenId, { amount: current.amount - amount, isMint: current.isMint })
outputsToAdmit.push(vout)
} catch { continue }
}
// Validate balance conservation
for (const [tokenId, balance] of balances) {
if (balance.isMint) continue // Mints can create new supply
if (balance.amount !== 0) {
console.log(`β Balance mismatch for ${tokenId}: ${balance.amount}`)
return { outputsToAdmit: [], coinsToRetain: [] } // REJECT
}
}
console.log(`β
Admitted ${outputsToAdmit.length} outputs`)
return { outputsToAdmit, coinsToRetain: [] }
} catch (error) {
console.error('Validation error:', error)
return { outputsToAdmit: [], coinsToRetain: [] }
}
}
getDocumentation(): string {
return 'Validates fungible token balance conservation using PushDrop (BRC-48)'
}
getMetaData() {
return {
name: 'tm_mytoken',
shortDescription: 'My Token Validator',
version: '1.0.0'
}
}
}Inputs: +amount (credit)
Outputs: -amount (debit)
Result: must equal 0 (except mints)import { MongoClient, Collection } from 'mongodb'
interface TokenRecord {
txid: string
outputIndex: number
tokenId: string
amount: string
metadata: any
createdAt: Date
}
export class TokenStorage {
private collection: Collection<TokenRecord> | null = null
async initialize(uri: string) {
const client = await MongoClient.connect(uri)
this.collection = client.db().collection('tokens')
// Create indices
await this.collection.createIndex({ txid: 1, outputIndex: 1 }, { unique: true })
await this.collection.createIndex({ tokenId: 'hashed' })
await this.collection.createIndex({ createdAt: -1 })
}
async storeRecord(txid: string, outputIndex: number, tokenId: string, amount: string, metadata: any) {
await this.collection!.insertOne({ txid, outputIndex, tokenId, amount, metadata, createdAt: new Date() })
}
async findByTokenId(tokenId: string, limit = 50, skip = 0) {
return await this.collection!.find({ tokenId }).sort({ createdAt: -1 }).skip(skip).limit(limit).toArray()
}
async findByOutpoint(txid: string, vout: number) {
return await this.collection!.findOne({ txid, outputIndex: vout })
}
}import { LookupService, LookupQuestion, LookupAnswer } from '@bsv/overlay-tools'
import { PushDrop, LockingScript, Utils } from '@bsv/sdk'
import { TokenStorage } from './TokenStorage'
export class MyTokenLookupService implements LookupService {
constructor(private storage: TokenStorage) {}
async outputAdded(payload: { txid: string, outputIndex: number, outputScript: { script: string } }) {
try {
const script = LockingScript.fromHex(payload.outputScript.script)
const token = PushDrop.decode(script)
let tokenId = Utils.toUTF8(token.fields[0])
if (tokenId === '___mint___') tokenId = `${payload.txid}.${payload.outputIndex}`
const amount = new Utils.Reader(token.fields[1]).readUInt64LEBn().toString()
const metadata = JSON.parse(Utils.toUTF8(token.fields[2]))
await this.storage.storeRecord(payload.txid, payload.outputIndex, tokenId, amount, metadata)
console.log(`π Indexed: ${tokenId}`)
} catch (error) {
console.error('Indexing error:', error)
}
}
async lookup(question: LookupQuestion): Promise<LookupAnswer> {
const query = question.query as any
if (query.outpoint) {
const [txid, vout] = query.outpoint.split('.')
const record = await this.storage.findByOutpoint(txid, parseInt(vout))
return { type: 'output-list', outputs: record ? [record] : [] }
}
if (query.tokenId) {
const records = await this.storage.findByTokenId(query.tokenId, query.limit, query.skip)
return { type: 'output-list', outputs: records }
}
return { type: 'output-list', outputs: [] }
}
getDocumentation() { return 'Query tokens by outpoint or tokenId' }
getMetaData() { return { name: 'ls_mytoken', shortDescription: 'Token Lookup', version: '1.0.0' } }
}import express from 'express'
import { config } from 'dotenv'
import { OverlayExpressNode } from '@bsv/overlay-express'
import { MyTokenTopicManager } from './MyTokenTopicManager'
import { MyTokenLookupService } from './MyTokenLookupService'
import { TokenStorage } from './TokenStorage'
config()
async function main() {
const app = express()
const storage = new TokenStorage()
await storage.initialize(process.env.MONGODB_URI!)
new OverlayExpressNode({
port: parseInt(process.env.PORT!),
app,
nodeImpl: {
getTopicManagers: () => [new MyTokenTopicManager()],
getLookupServiceFactory: () => ({ getLookupService: () => new MyTokenLookupService(storage) })
}
})
app.listen(process.env.PORT, () => {
console.log(`β
Overlay running on http://localhost:${process.env.PORT}`)
console.log(`π‘ POST /submit - Submit transactions`)
console.log(`π POST /lookup - Query tokens`)
})
}
main()import { ChaintracksChainTracker } from '@bsv/sdk'
const chainTracker = new ChaintracksChainTracker('main') // or 'test'
server.configureChainTracker(chainTracker)mongod --dbpath ./datanpm run build && npm startimport { HTTPSOverlayBroadcastFacilitator } from '@bsv/sdk'
const overlay = new HTTPSOverlayBroadcastFacilitator()
const response = await overlay.send('http://localhost:3000/submit', {
beef: tx.toBEEF(),
topics: ['tm_mytoken']
})
// Returns: { tm_mytoken: { outputsToAdmit: [0, 1], coinsToRetain: [] } }curl -X POST http://localhost:3000/lookup -H "Content-Type: application/json" \
-d '{"service":"ls_mytoken","query":{"tokenId":"abc.0","limit":50}}'Valid: Input +1000 β Outputs -700, -300 = 0 β
Invalid: Input +1000 β Output -1200 = -200 β
Mint: No inputs β Output +1000 (isMint=true) β
async outputSpent(payload: { txid: string, outputIndex: number }) {
await this.storage.markAsSpent(payload.txid, payload.outputIndex)
}const metadata = JSON.parse(Utils.toUTF8(token.fields[2]))
if (!metadata.label) return { outputsToAdmit: [], coinsToRetain: [] }const allowedTokens = ['credits', 'points']
if (!allowedTokens.includes(label)) return { outputsToAdmit: [], coinsToRetain: [] }