feat: bridges, PMM, flash workflow, token-aggregation, and deployment docs
- CCIP/trustless bridge contracts, GRU tokens, DEX/PMM tests, reserve vault. - Token-aggregation service routes, planner, chain config, relay env templates. - Config snapshots and multi-chain deployment markdown updates. - gitignore services/btc-intake/dist/ (tsc output); do not track dist. Run forge build && forge test before deploy (large solc graph). Made-with: Cursor
This commit is contained in:
36
services/btc-intake/src/custody-adapter.ts
Normal file
36
services/btc-intake/src/custody-adapter.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { BitcoinDepositEvent } from './types';
|
||||
|
||||
export interface CustodyAdapter {
|
||||
allocateDepositAddress(input: { instructionId: string; clientId: string }): Promise<string>;
|
||||
listDepositEvents(cursor?: string): Promise<{ events: BitcoinDepositEvent[]; cursor?: string }>;
|
||||
getConfirmedReserveBalanceSats(): Promise<number>;
|
||||
}
|
||||
|
||||
export interface MintJobSink {
|
||||
enqueue(job: {
|
||||
id: string;
|
||||
instructionId: string;
|
||||
basketMandateId: string;
|
||||
chain138VaultAddress: string;
|
||||
canonicalSymbol: 'cBTC';
|
||||
amountSats: number;
|
||||
status: 'queued';
|
||||
createdAt: string;
|
||||
}): Promise<void>;
|
||||
}
|
||||
|
||||
export interface AuditPublisher {
|
||||
publish(record: {
|
||||
id: string;
|
||||
instructionId: string;
|
||||
category: 'deposit_instruction' | 'confirmation' | 'mint_job' | 'freeze';
|
||||
message: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
}): Promise<void>;
|
||||
}
|
||||
|
||||
export interface OutstandingPolicy {
|
||||
getCurrentOutstandingSats(): Promise<number>;
|
||||
getMaxOutstandingSats(): Promise<number>;
|
||||
}
|
||||
3
services/btc-intake/src/index.ts
Normal file
3
services/btc-intake/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './types';
|
||||
export * from './custody-adapter';
|
||||
export * from './native-bitcoin-watcher';
|
||||
267
services/btc-intake/src/native-bitcoin-watcher.test.ts
Normal file
267
services/btc-intake/src/native-bitcoin-watcher.test.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type {
|
||||
AuditPublisher,
|
||||
CustodyAdapter,
|
||||
MintJobSink,
|
||||
OutstandingPolicy,
|
||||
} from './custody-adapter';
|
||||
import { NativeBitcoinWatcher } from './native-bitcoin-watcher';
|
||||
import type { AuditRecord, BitcoinDepositEvent, MintJob } from './types';
|
||||
|
||||
class FakeCustodyAdapter implements CustodyAdapter {
|
||||
reserveBalanceSats = 0;
|
||||
private events: BitcoinDepositEvent[] = [];
|
||||
private cursor = 0;
|
||||
|
||||
async allocateDepositAddress(input: { instructionId: string }): Promise<string> {
|
||||
return `bc1q${input.instructionId.replace(/-/g, '').slice(0, 20)}`;
|
||||
}
|
||||
|
||||
queueEvent(event: BitcoinDepositEvent): void {
|
||||
this.events.push(event);
|
||||
}
|
||||
|
||||
async listDepositEvents(cursor?: string): Promise<{ events: BitcoinDepositEvent[]; cursor?: string }> {
|
||||
const start = cursor ? Number(cursor) : this.cursor;
|
||||
const nextEvents = this.events.slice(start);
|
||||
const nextCursor = String(this.events.length);
|
||||
this.cursor = this.events.length;
|
||||
return { events: nextEvents, cursor: nextCursor };
|
||||
}
|
||||
|
||||
async getConfirmedReserveBalanceSats(): Promise<number> {
|
||||
return this.reserveBalanceSats;
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryMintSink implements MintJobSink {
|
||||
readonly jobs: MintJob[] = [];
|
||||
|
||||
async enqueue(job: MintJob): Promise<void> {
|
||||
this.jobs.push(job);
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryAuditPublisher implements AuditPublisher {
|
||||
readonly records: AuditRecord[] = [];
|
||||
|
||||
async publish(record: AuditRecord): Promise<void> {
|
||||
this.records.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
class StaticOutstandingPolicy implements OutstandingPolicy {
|
||||
constructor(
|
||||
private readonly currentOutstandingSats: number,
|
||||
private readonly maxOutstandingSats: number,
|
||||
) {}
|
||||
|
||||
async getCurrentOutstandingSats(): Promise<number> {
|
||||
return this.currentOutstandingSats;
|
||||
}
|
||||
|
||||
async getMaxOutstandingSats(): Promise<number> {
|
||||
return this.maxOutstandingSats;
|
||||
}
|
||||
}
|
||||
|
||||
describe('NativeBitcoinWatcher', () => {
|
||||
it('allocates unique deposit addresses', async () => {
|
||||
const adapter = new FakeCustodyAdapter();
|
||||
const mintSink = new InMemoryMintSink();
|
||||
const audit = new InMemoryAuditPublisher();
|
||||
const watcher = new NativeBitcoinWatcher(adapter, mintSink, audit);
|
||||
|
||||
const instruction = await watcher.createDepositInstruction({
|
||||
basketMandate: {
|
||||
id: 'basket-1',
|
||||
clientId: 'client-1',
|
||||
chain138VaultAddress: '0x1111111111111111111111111111111111111111',
|
||||
allocations: [{ symbol: 'cBTC', targetWeightBps: 10_000 }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(instruction.depositAddress.startsWith('bc1q')).toBe(true);
|
||||
expect(audit.records[0]?.category).toBe('deposit_instruction');
|
||||
});
|
||||
|
||||
it('auto-mints after 6 confirmations and emits one mint job', async () => {
|
||||
const adapter = new FakeCustodyAdapter();
|
||||
adapter.reserveBalanceSats = 300_000_000;
|
||||
const mintSink = new InMemoryMintSink();
|
||||
const audit = new InMemoryAuditPublisher();
|
||||
const watcher = new NativeBitcoinWatcher(adapter, mintSink, audit);
|
||||
|
||||
const instruction = await watcher.createDepositInstruction({
|
||||
basketMandate: {
|
||||
id: 'basket-2',
|
||||
clientId: 'client-2',
|
||||
chain138VaultAddress: '0x2222222222222222222222222222222222222222',
|
||||
allocations: [{ symbol: 'cBTC', targetWeightBps: 10_000 }],
|
||||
},
|
||||
expectedAmountSats: 250_000_000,
|
||||
});
|
||||
|
||||
adapter.queueEvent({
|
||||
instructionId: instruction.id,
|
||||
txId: 'tx-1',
|
||||
confirmations: 6,
|
||||
amountSats: 250_000_000,
|
||||
type: 'deposit',
|
||||
});
|
||||
|
||||
await watcher.sync();
|
||||
|
||||
expect(watcher.getInstruction(instruction.id)?.status).toBe('minted');
|
||||
expect(mintSink.jobs).toHaveLength(1);
|
||||
expect(mintSink.jobs[0]).toMatchObject({
|
||||
instructionId: instruction.id,
|
||||
amountSats: 250_000_000,
|
||||
canonicalSymbol: 'cBTC',
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores duplicate finality events', async () => {
|
||||
const adapter = new FakeCustodyAdapter();
|
||||
adapter.reserveBalanceSats = 150_000_000;
|
||||
const mintSink = new InMemoryMintSink();
|
||||
const audit = new InMemoryAuditPublisher();
|
||||
const watcher = new NativeBitcoinWatcher(adapter, mintSink, audit);
|
||||
|
||||
const instruction = await watcher.createDepositInstruction({
|
||||
basketMandate: {
|
||||
id: 'basket-3',
|
||||
clientId: 'client-3',
|
||||
chain138VaultAddress: '0x3333333333333333333333333333333333333333',
|
||||
allocations: [{ symbol: 'cBTC', targetWeightBps: 10_000 }],
|
||||
},
|
||||
});
|
||||
|
||||
adapter.queueEvent({
|
||||
instructionId: instruction.id,
|
||||
txId: 'tx-dup',
|
||||
confirmations: 6,
|
||||
amountSats: 125_000_000,
|
||||
type: 'deposit',
|
||||
});
|
||||
adapter.queueEvent({
|
||||
instructionId: instruction.id,
|
||||
txId: 'tx-dup',
|
||||
confirmations: 6,
|
||||
amountSats: 125_000_000,
|
||||
type: 'deposit',
|
||||
});
|
||||
|
||||
await watcher.sync();
|
||||
|
||||
expect(mintSink.jobs).toHaveLength(1);
|
||||
expect(watcher.getInstruction(instruction.id)?.status).toBe('minted');
|
||||
});
|
||||
|
||||
it('handles reorgs before finality without minting early', async () => {
|
||||
const adapter = new FakeCustodyAdapter();
|
||||
adapter.reserveBalanceSats = 500_000_000;
|
||||
const mintSink = new InMemoryMintSink();
|
||||
const audit = new InMemoryAuditPublisher();
|
||||
const watcher = new NativeBitcoinWatcher(adapter, mintSink, audit);
|
||||
|
||||
const instruction = await watcher.createDepositInstruction({
|
||||
basketMandate: {
|
||||
id: 'basket-4',
|
||||
clientId: 'client-4',
|
||||
chain138VaultAddress: '0x4444444444444444444444444444444444444444',
|
||||
allocations: [{ symbol: 'cBTC', targetWeightBps: 10_000 }],
|
||||
},
|
||||
});
|
||||
|
||||
adapter.queueEvent({
|
||||
instructionId: instruction.id,
|
||||
txId: 'tx-reorg',
|
||||
confirmations: 5,
|
||||
amountSats: 100_000_000,
|
||||
type: 'deposit',
|
||||
});
|
||||
adapter.queueEvent({
|
||||
instructionId: instruction.id,
|
||||
txId: 'tx-reorg',
|
||||
confirmations: 2,
|
||||
amountSats: 100_000_000,
|
||||
type: 'reorg',
|
||||
});
|
||||
adapter.queueEvent({
|
||||
instructionId: instruction.id,
|
||||
txId: 'tx-reorg',
|
||||
confirmations: 6,
|
||||
amountSats: 100_000_000,
|
||||
type: 'deposit',
|
||||
});
|
||||
|
||||
await watcher.sync();
|
||||
|
||||
expect(mintSink.jobs).toHaveLength(1);
|
||||
expect(watcher.getInstruction(instruction.id)?.status).toBe('minted');
|
||||
});
|
||||
|
||||
it('freezes when reserve reconciliation fails', async () => {
|
||||
const adapter = new FakeCustodyAdapter();
|
||||
adapter.reserveBalanceSats = 99_999_999;
|
||||
const mintSink = new InMemoryMintSink();
|
||||
const audit = new InMemoryAuditPublisher();
|
||||
const watcher = new NativeBitcoinWatcher(adapter, mintSink, audit);
|
||||
|
||||
const instruction = await watcher.createDepositInstruction({
|
||||
basketMandate: {
|
||||
id: 'basket-5',
|
||||
clientId: 'client-5',
|
||||
chain138VaultAddress: '0x5555555555555555555555555555555555555555',
|
||||
allocations: [{ symbol: 'cBTC', targetWeightBps: 10_000 }],
|
||||
},
|
||||
});
|
||||
|
||||
adapter.queueEvent({
|
||||
instructionId: instruction.id,
|
||||
txId: 'tx-deficit',
|
||||
confirmations: 6,
|
||||
amountSats: 100_000_000,
|
||||
type: 'deposit',
|
||||
});
|
||||
|
||||
await watcher.sync();
|
||||
|
||||
expect(mintSink.jobs).toHaveLength(0);
|
||||
expect(watcher.getInstruction(instruction.id)?.status).toBe('frozen');
|
||||
expect(audit.records[audit.records.length - 1]?.category).toBe('freeze');
|
||||
});
|
||||
|
||||
it('freezes when outstanding bridge capacity would be exceeded', async () => {
|
||||
const adapter = new FakeCustodyAdapter();
|
||||
adapter.reserveBalanceSats = 500_000_000;
|
||||
const mintSink = new InMemoryMintSink();
|
||||
const audit = new InMemoryAuditPublisher();
|
||||
const outstanding = new StaticOutstandingPolicy(450_000_000, 500_000_000);
|
||||
const watcher = new NativeBitcoinWatcher(adapter, mintSink, audit, outstanding);
|
||||
|
||||
const instruction = await watcher.createDepositInstruction({
|
||||
basketMandate: {
|
||||
id: 'basket-6',
|
||||
clientId: 'client-6',
|
||||
chain138VaultAddress: '0x6666666666666666666666666666666666666666',
|
||||
allocations: [{ symbol: 'cBTC', targetWeightBps: 10_000 }],
|
||||
},
|
||||
});
|
||||
|
||||
adapter.queueEvent({
|
||||
instructionId: instruction.id,
|
||||
txId: 'tx-cap',
|
||||
confirmations: 6,
|
||||
amountSats: 75_000_000,
|
||||
type: 'deposit',
|
||||
});
|
||||
|
||||
await watcher.sync();
|
||||
|
||||
expect(mintSink.jobs).toHaveLength(0);
|
||||
expect(watcher.getInstruction(instruction.id)?.status).toBe('frozen');
|
||||
expect(watcher.getInstruction(instruction.id)?.freezeReason).toContain('Bridge outstanding limit');
|
||||
});
|
||||
});
|
||||
250
services/btc-intake/src/native-bitcoin-watcher.ts
Normal file
250
services/btc-intake/src/native-bitcoin-watcher.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import type {
|
||||
AuditPublisher,
|
||||
CustodyAdapter,
|
||||
MintJobSink,
|
||||
OutstandingPolicy,
|
||||
} from './custody-adapter';
|
||||
import type {
|
||||
AuditRecord,
|
||||
BasketMandateSnapshot,
|
||||
BitcoinDepositEvent,
|
||||
DepositInstruction,
|
||||
MintJob,
|
||||
} from './types';
|
||||
|
||||
const DEFAULT_CONFIRMATIONS_REQUIRED = 6;
|
||||
|
||||
function nowIso(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function clone<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
export class NativeBitcoinWatcher {
|
||||
private readonly instructions = new Map<string, DepositInstruction>();
|
||||
private readonly mintJobs = new Map<string, MintJob>();
|
||||
private readonly auditRecords: AuditRecord[] = [];
|
||||
private cursor?: string;
|
||||
|
||||
constructor(
|
||||
private readonly adapter: CustodyAdapter,
|
||||
private readonly mintSink: MintJobSink,
|
||||
private readonly auditPublisher: AuditPublisher,
|
||||
private readonly outstandingPolicy?: OutstandingPolicy,
|
||||
) {}
|
||||
|
||||
async createDepositInstruction(input: {
|
||||
basketMandate: BasketMandateSnapshot;
|
||||
expectedAmountSats?: number;
|
||||
}): Promise<DepositInstruction> {
|
||||
const instructionId = randomUUID();
|
||||
const depositAddress = await this.adapter.allocateDepositAddress({
|
||||
instructionId,
|
||||
clientId: input.basketMandate.clientId,
|
||||
});
|
||||
const timestamp = nowIso();
|
||||
|
||||
const instruction: DepositInstruction = {
|
||||
id: instructionId,
|
||||
basketMandate: clone(input.basketMandate),
|
||||
depositAddress,
|
||||
expectedAmountSats: input.expectedAmountSats,
|
||||
currentConfirmations: 0,
|
||||
confirmationsRequired: DEFAULT_CONFIRMATIONS_REQUIRED,
|
||||
status: 'instruction_created',
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
|
||||
this.instructions.set(instruction.id, instruction);
|
||||
await this.publishAudit({
|
||||
id: randomUUID(),
|
||||
instructionId,
|
||||
category: 'deposit_instruction',
|
||||
message: 'Allocated BTC deposit address for Chain 138 cBTC mint flow',
|
||||
metadata: {
|
||||
depositAddress,
|
||||
basketMandateId: input.basketMandate.id,
|
||||
},
|
||||
createdAt: timestamp,
|
||||
});
|
||||
|
||||
return clone(instruction);
|
||||
}
|
||||
|
||||
getInstruction(id: string): DepositInstruction | null {
|
||||
const instruction = this.instructions.get(id);
|
||||
return instruction ? clone(instruction) : null;
|
||||
}
|
||||
|
||||
getAuditRecords(): AuditRecord[] {
|
||||
return clone(this.auditRecords);
|
||||
}
|
||||
|
||||
getMintJobs(): MintJob[] {
|
||||
return Array.from(this.mintJobs.values()).map((job) => clone(job));
|
||||
}
|
||||
|
||||
async sync(): Promise<DepositInstruction[]> {
|
||||
const { events, cursor } = await this.adapter.listDepositEvents(this.cursor);
|
||||
for (const event of events) {
|
||||
await this.applyEvent(event);
|
||||
}
|
||||
this.cursor = cursor;
|
||||
return Array.from(this.instructions.values()).map((instruction) => clone(instruction));
|
||||
}
|
||||
|
||||
private async applyEvent(event: BitcoinDepositEvent): Promise<void> {
|
||||
const instruction = this.instructions.get(event.instructionId);
|
||||
if (!instruction) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
instruction.observedTxId === event.txId &&
|
||||
instruction.currentConfirmations >= event.confirmations &&
|
||||
instruction.status !== 'frozen'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
instruction.observedTxId = event.txId;
|
||||
instruction.currentConfirmations = Math.max(0, event.confirmations);
|
||||
instruction.expectedAmountSats = event.amountSats;
|
||||
instruction.updatedAt = event.observedAt || nowIso();
|
||||
|
||||
if (event.type === 'reorg' || event.confirmations < instruction.confirmationsRequired) {
|
||||
instruction.status = 'pending_confirmations';
|
||||
await this.publishAudit({
|
||||
id: randomUUID(),
|
||||
instructionId: instruction.id,
|
||||
category: 'confirmation',
|
||||
message: 'Deposit observed but waiting for final BTC confirmations',
|
||||
metadata: {
|
||||
txId: event.txId,
|
||||
confirmations: event.confirmations,
|
||||
type: event.type,
|
||||
},
|
||||
createdAt: instruction.updatedAt,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
instruction.status = 'confirmed';
|
||||
await this.publishAudit({
|
||||
id: randomUUID(),
|
||||
instructionId: instruction.id,
|
||||
category: 'confirmation',
|
||||
message: 'Deposit reached 6 confirmations and is eligible for minting',
|
||||
metadata: {
|
||||
txId: event.txId,
|
||||
confirmations: event.confirmations,
|
||||
amountSats: event.amountSats,
|
||||
},
|
||||
createdAt: instruction.updatedAt,
|
||||
});
|
||||
|
||||
await this.maybeQueueMint(instruction, event.amountSats);
|
||||
}
|
||||
|
||||
private async maybeQueueMint(instruction: DepositInstruction, amountSats: number): Promise<void> {
|
||||
if (this.mintJobs.has(instruction.id)) {
|
||||
instruction.status = 'minted';
|
||||
return;
|
||||
}
|
||||
|
||||
const reserveBalanceSats = await this.adapter.getConfirmedReserveBalanceSats();
|
||||
const alreadyMintedSats = Array.from(this.mintJobs.values()).reduce(
|
||||
(total, job) => total + job.amountSats,
|
||||
0,
|
||||
);
|
||||
|
||||
if (reserveBalanceSats < alreadyMintedSats + amountSats) {
|
||||
await this.freezeInstruction(
|
||||
instruction,
|
||||
'Reserve deficit detected during cBTC reconciliation',
|
||||
{
|
||||
reserveBalanceSats,
|
||||
alreadyMintedSats,
|
||||
requestedMintSats: amountSats,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.outstandingPolicy) {
|
||||
const currentOutstandingSats = await this.outstandingPolicy.getCurrentOutstandingSats();
|
||||
const maxOutstandingSats = await this.outstandingPolicy.getMaxOutstandingSats();
|
||||
|
||||
if (maxOutstandingSats > 0 && currentOutstandingSats + amountSats > maxOutstandingSats) {
|
||||
await this.freezeInstruction(
|
||||
instruction,
|
||||
'Bridge outstanding limit would be exceeded by mint request',
|
||||
{
|
||||
currentOutstandingSats,
|
||||
maxOutstandingSats,
|
||||
requestedMintSats: amountSats,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const timestamp = nowIso();
|
||||
const mintJob: MintJob = {
|
||||
id: randomUUID(),
|
||||
instructionId: instruction.id,
|
||||
basketMandateId: instruction.basketMandate.id,
|
||||
chain138VaultAddress: instruction.basketMandate.chain138VaultAddress,
|
||||
canonicalSymbol: 'cBTC',
|
||||
amountSats,
|
||||
status: 'queued',
|
||||
createdAt: timestamp,
|
||||
};
|
||||
|
||||
await this.mintSink.enqueue(mintJob);
|
||||
this.mintJobs.set(instruction.id, mintJob);
|
||||
instruction.status = 'minted';
|
||||
instruction.updatedAt = timestamp;
|
||||
|
||||
await this.publishAudit({
|
||||
id: randomUUID(),
|
||||
instructionId: instruction.id,
|
||||
category: 'mint_job',
|
||||
message: 'Queued auto-mint job for confirmed BTC deposit',
|
||||
metadata: {
|
||||
mintJobId: mintJob.id,
|
||||
chain138VaultAddress: mintJob.chain138VaultAddress,
|
||||
amountSats,
|
||||
},
|
||||
createdAt: timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
private async freezeInstruction(
|
||||
instruction: DepositInstruction,
|
||||
reason: string,
|
||||
metadata?: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
instruction.status = 'frozen';
|
||||
instruction.freezeReason = reason;
|
||||
instruction.updatedAt = nowIso();
|
||||
|
||||
await this.publishAudit({
|
||||
id: randomUUID(),
|
||||
instructionId: instruction.id,
|
||||
category: 'freeze',
|
||||
message: reason,
|
||||
metadata,
|
||||
createdAt: instruction.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
private async publishAudit(record: AuditRecord): Promise<void> {
|
||||
this.auditRecords.push(record);
|
||||
await this.auditPublisher.publish(record);
|
||||
}
|
||||
}
|
||||
59
services/btc-intake/src/types.ts
Normal file
59
services/btc-intake/src/types.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
export type DepositInstructionStatus =
|
||||
| 'instruction_created'
|
||||
| 'pending_confirmations'
|
||||
| 'confirmed'
|
||||
| 'minted'
|
||||
| 'frozen';
|
||||
|
||||
export interface BasketMandateSnapshot {
|
||||
id: string;
|
||||
clientId: string;
|
||||
chain138VaultAddress: string;
|
||||
allocations: Array<{
|
||||
symbol: string;
|
||||
targetWeightBps: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface DepositInstruction {
|
||||
id: string;
|
||||
basketMandate: BasketMandateSnapshot;
|
||||
depositAddress: string;
|
||||
expectedAmountSats?: number;
|
||||
currentConfirmations: number;
|
||||
confirmationsRequired: number;
|
||||
status: DepositInstructionStatus;
|
||||
observedTxId?: string;
|
||||
freezeReason?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BitcoinDepositEvent {
|
||||
instructionId: string;
|
||||
txId: string;
|
||||
confirmations: number;
|
||||
amountSats: number;
|
||||
type: 'deposit' | 'reorg';
|
||||
observedAt?: string;
|
||||
}
|
||||
|
||||
export interface MintJob {
|
||||
id: string;
|
||||
instructionId: string;
|
||||
basketMandateId: string;
|
||||
chain138VaultAddress: string;
|
||||
canonicalSymbol: 'cBTC';
|
||||
amountSats: number;
|
||||
status: 'queued';
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface AuditRecord {
|
||||
id: string;
|
||||
instructionId: string;
|
||||
category: 'deposit_instruction' | 'confirmation' | 'mint_job' | 'freeze';
|
||||
message: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
}
|
||||
Reference in New Issue
Block a user