Skip to content

Commit c5b5f07

Browse files
committed
feat: wire real facilitator + payout service + deposit refund
Replace stubbed x402 facilitator with real HTTPFacilitatorClient. Wire PayoutService through app deps to sell and treasury routes. Implement deposit callback with USDC payout and SOL refund on failure.
1 parent 7d22a32 commit c5b5f07

3 files changed

Lines changed: 95 additions & 13 deletions

File tree

src/app.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createApp } from './app.js';
33
import { PricingService } from './services/pricing.js';
44
import { TransactionDB } from './db/sqlite.js';
55
import { X402Service } from './services/x402.js';
6+
import type { PayoutService } from './services/payout.js';
67

78
function makeDeps() {
89
const db = new TransactionDB(':memory:');
@@ -30,6 +31,15 @@ function makeX402() {
3031
});
3132
}
3233

34+
function makePayoutStub(): PayoutService {
35+
return {
36+
walletAddress: 'PayoutWa11et1111111111111111111111111111111',
37+
getUsdcBalance: async () => 500,
38+
canAffordPayout: async () => true,
39+
sendUsdc: async () => 'fakePayoutSig123',
40+
} as unknown as PayoutService;
41+
}
42+
3343
describe('DevSOL App', () => {
3444
it('GET /health returns ok', async () => {
3545
const { db, pricing } = makeDeps();
@@ -162,4 +172,37 @@ describe('DevSOL App', () => {
162172

163173
db.close();
164174
});
175+
176+
it('accepts payout as optional dep and wires to routes', async () => {
177+
const { db, pricing } = makeDeps();
178+
const treasury = makeTreasuryStub();
179+
const x402 = makeX402();
180+
const payout = makePayoutStub();
181+
const { app } = createApp({
182+
pricing,
183+
db,
184+
treasury: treasury as any,
185+
x402,
186+
payout,
187+
});
188+
189+
// Sell should work with payout wired
190+
const sellRes = await app.request('/sell', {
191+
method: 'POST',
192+
headers: { 'Content-Type': 'application/json' },
193+
body: JSON.stringify({ wallet: 'TestWa11et111XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', amount_sol: 1 }),
194+
});
195+
expect(sellRes.status).toBe(200);
196+
const sellBody = await sellRes.json();
197+
expect(sellBody.status).toBe('pending');
198+
199+
// Health detail should include payout info
200+
const healthRes = await app.request('/health/detail');
201+
expect(healthRes.status).toBe(200);
202+
const healthBody = await healthRes.json();
203+
expect(healthBody.payout_usdc).toBe(500);
204+
expect(healthBody.payout_wallet).toBe('PayoutWa11et1111111111111111111111111111111');
205+
206+
db.close();
207+
});
165208
});

src/app.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { sellRoutes } from './routes/sell.js';
99
import { PricingService } from './services/pricing.js';
1010
import type { TreasuryService } from './services/treasury.js';
1111
import type { X402Service } from './services/x402.js';
12+
import type { PayoutService } from './services/payout.js';
1213
import { TransactionDB } from './db/sqlite.js';
1314
import { config } from './config.js';
1415

@@ -21,6 +22,7 @@ interface AppDeps {
2122
treasury?: TreasuryService;
2223
db?: TransactionDB;
2324
x402?: X402Service;
25+
payout?: PayoutService;
2426
}
2527

2628
export function createApp(deps?: AppDeps) {
@@ -62,9 +64,9 @@ export function createApp(deps?: AppDeps) {
6264
app.route('/', txRoutes(db));
6365

6466
if (deps?.treasury && deps?.x402) {
65-
app.route('/', treasuryRoutes(deps.treasury));
67+
app.route('/', treasuryRoutes(deps.treasury, deps.payout));
6668
app.route('/', buyRoutes({ db, pricing, treasury: deps.treasury, x402: deps.x402 }));
67-
app.route('/', sellRoutes({ db, pricing, treasuryAddress: deps.treasury.address }));
69+
app.route('/', sellRoutes({ db, pricing, treasuryAddress: deps.treasury.address, payout: deps.payout }));
6870
}
6971

7072
return { app, db, pricing };

src/index.ts

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,84 @@
11
import { serve } from '@hono/node-server';
22
import { readFileSync } from 'fs';
33
import { createSolanaRpc } from '@solana/kit';
4+
import { HTTPFacilitatorClient } from '@x402/core/http';
45
import { createApp } from './app.js';
56
import { TreasuryService } from './services/treasury.js';
67
import { X402Service } from './services/x402.js';
8+
import { PayoutService } from './services/payout.js';
79
import { DepositDetector } from './services/deposit.js';
810
import { config } from './config.js';
911

1012
async function main() {
13+
// Devnet treasury (SOL)
1114
const keypairJson = readFileSync(config.treasuryKeypair, 'utf-8');
1215
const keypairBytes = new Uint8Array(JSON.parse(keypairJson));
13-
1416
const treasury = await TreasuryService.create({
1517
rpcUrl: config.devnetRpc,
1618
wssUrl: config.devnetWss,
1719
keypairBytes,
1820
});
1921

22+
// x402 facilitator (real)
23+
const facilitator = new HTTPFacilitatorClient({ url: config.facilitatorUrl });
2024
const x402 = new X402Service({
21-
facilitator: {
22-
verify: async () => ({ isValid: true }),
23-
settle: async () => ({ success: true, transaction: '', network: config.svmNetwork as `${string}:${string}` }),
24-
getSupported: async () => ({ kinds: [], extensions: [], signers: {} }),
25-
},
25+
facilitator,
2626
payTo: treasury.address,
2727
network: config.svmNetwork,
2828
});
29-
console.warn('WARNING: x402 facilitator is STUBBED — wire HTTPFacilitatorClient for production');
3029

31-
const { app, db } = createApp({ treasury, x402 });
30+
// Mainnet payout (USDC) — optional, enables sell flow
31+
let payout: PayoutService | undefined;
32+
if (config.mainnetKeypair) {
33+
const mainnetJson = readFileSync(config.mainnetKeypair, 'utf-8');
34+
const mainnetBytes = new Uint8Array(JSON.parse(mainnetJson));
35+
payout = await PayoutService.create({
36+
rpcUrl: config.mainnetRpc,
37+
wssUrl: config.mainnetWss,
38+
keypairBytes: mainnetBytes,
39+
maxPayoutUsdc: config.maxPayoutUsdc,
40+
minReserveUsdc: config.minReserveUsdc,
41+
});
42+
console.log(`Payout wallet: ${payout.walletAddress}`);
43+
} else {
44+
console.warn('WARNING: No mainnet keypair configured — sell payouts disabled');
45+
}
46+
47+
const { app, db } = createApp({ treasury, x402, payout });
3248

49+
// Deposit detector with payout callback
3350
const devnetRpc = createSolanaRpc(config.devnetRpc);
3451
const depositDetector = new DepositDetector({
3552
db,
3653
rpc: devnetRpc as any,
3754
treasuryAddress: treasury.address,
38-
onDeposit: async (tx, sig) => {
39-
console.log(`Deposit confirmed for sell ${tx.id}: ${sig}`);
40-
// TODO: trigger USDC payout on mainnet
55+
onDeposit: async (tx, devnetSig) => {
56+
console.log(`Deposit confirmed for sell ${tx.id}: ${devnetSig}`);
57+
if (!payout) {
58+
console.warn(`No payout service — sell ${tx.id} completed without USDC payout`);
59+
return;
60+
}
61+
try {
62+
const mainnetSig = await payout.sendUsdc(tx.wallet, tx.usdc_amount);
63+
db.update(tx.id, { mainnet_payout_tx: mainnetSig });
64+
console.log(`USDC payout sent for sell ${tx.id}: ${mainnetSig}`);
65+
} catch (err) {
66+
console.error(`USDC payout failed for sell ${tx.id}:`, err);
67+
// Refund devnet SOL
68+
try {
69+
const refundSig = await treasury.sendSol(tx.wallet, tx.sol_amount);
70+
db.update(tx.id, { status: 'refunded', devnet_tx: refundSig });
71+
console.log(`Refunded ${tx.sol_amount} SOL to ${tx.wallet}: ${refundSig}`);
72+
} catch (refundErr) {
73+
console.error(`CRITICAL: Refund also failed for sell ${tx.id}:`, refundErr);
74+
db.update(tx.id, { status: 'failed' });
75+
}
76+
}
4177
},
4278
});
4379
depositDetector.start();
4480

81+
// Graceful shutdown
4582
const shutdown = () => {
4683
depositDetector.stop();
4784
db.close();

0 commit comments

Comments
 (0)