@wopr-network/platform-core 1.66.0 → 1.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/billing/crypto/__tests__/key-server.test.js +379 -0
- package/dist/billing/crypto/key-server.js +129 -5
- package/drizzle/migrations/0000_slippery_mandrill.sql +133 -133
- package/drizzle/migrations/0001_infrastructure_extraction.sql +102 -102
- package/drizzle/migrations/0002_gateway_service_keys.sql +4 -4
- package/drizzle/migrations/0003_double_entry_ledger.sql +15 -15
- package/drizzle/migrations/0005_stablecoin_columns.sql +7 -7
- package/drizzle/migrations/0006_invite_acceptance.sql +2 -2
- package/drizzle/migrations/0010_oracle_address.sql +2 -2
- package/drizzle/migrations/0011_notification_templates.sql +1 -1
- package/drizzle/migrations/0014_crypto_key_server.sql +2 -2
- package/drizzle/migrations/0015_callback_url.sql +3 -3
- package/drizzle/migrations/0016_charge_progress_columns.sql +4 -4
- package/drizzle/migrations/0020_encoding_params_column.sql +1 -1
- package/drizzle/migrations/0021_watcher_type_column.sql +1 -1
- package/drizzle/migrations/0022_oracle_asset_id_column.sql +1 -1
- package/package.json +1 -1
- package/src/billing/crypto/__tests__/key-server.test.ts +428 -0
- package/src/billing/crypto/key-server.ts +178 -5
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
-- Adds atomic derivation counter + path registry + address log.
|
|
3
3
|
|
|
4
4
|
-- 1. Add network column to payment_methods (parallel to chain)
|
|
5
|
-
ALTER TABLE "payment_methods" ADD COLUMN "network" text NOT NULL DEFAULT 'mainnet';
|
|
5
|
+
ALTER TABLE "payment_methods" ADD COLUMN IF NOT EXISTS "network" text NOT NULL DEFAULT 'mainnet';
|
|
6
6
|
--> statement-breakpoint
|
|
7
7
|
|
|
8
8
|
-- 2. Add next_index atomic counter to payment_methods
|
|
9
|
-
ALTER TABLE "payment_methods" ADD COLUMN "next_index" integer NOT NULL DEFAULT 0;
|
|
9
|
+
ALTER TABLE "payment_methods" ADD COLUMN IF NOT EXISTS "next_index" integer NOT NULL DEFAULT 0;
|
|
10
10
|
--> statement-breakpoint
|
|
11
11
|
|
|
12
12
|
-- 3. BIP-44 path allocation registry
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
-- Watcher service schema additions: webhook outbox + charge amount tracking.
|
|
2
2
|
|
|
3
3
|
-- 1. callback_url for webhook delivery
|
|
4
|
-
ALTER TABLE "crypto_charges" ADD COLUMN "callback_url" text;
|
|
4
|
+
ALTER TABLE "crypto_charges" ADD COLUMN IF NOT EXISTS "callback_url" text;
|
|
5
5
|
--> statement-breakpoint
|
|
6
6
|
|
|
7
7
|
-- 2. Expected crypto amount in native base units (locked at charge creation)
|
|
8
|
-
ALTER TABLE "crypto_charges" ADD COLUMN "expected_amount" text;
|
|
8
|
+
ALTER TABLE "crypto_charges" ADD COLUMN IF NOT EXISTS "expected_amount" text;
|
|
9
9
|
--> statement-breakpoint
|
|
10
10
|
|
|
11
11
|
-- 3. Running total of received crypto in native base units (partial payments)
|
|
12
|
-
ALTER TABLE "crypto_charges" ADD COLUMN "received_amount" text;
|
|
12
|
+
ALTER TABLE "crypto_charges" ADD COLUMN IF NOT EXISTS "received_amount" text;
|
|
13
13
|
--> statement-breakpoint
|
|
14
14
|
|
|
15
15
|
-- 4. Webhook delivery outbox — durable retry for payment callbacks
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
ALTER TABLE "crypto_charges" ADD COLUMN "confirmations" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
|
2
|
-
ALTER TABLE "crypto_charges" ADD COLUMN "confirmations_required" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
|
|
3
|
-
ALTER TABLE "crypto_charges" ADD COLUMN "tx_hash" text;--> statement-breakpoint
|
|
4
|
-
ALTER TABLE "crypto_charges" ADD COLUMN "amount_received_cents" integer DEFAULT 0 NOT NULL;
|
|
1
|
+
ALTER TABLE "crypto_charges" ADD COLUMN IF NOT EXISTS "confirmations" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
|
2
|
+
ALTER TABLE "crypto_charges" ADD COLUMN IF NOT EXISTS "confirmations_required" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
|
|
3
|
+
ALTER TABLE "crypto_charges" ADD COLUMN IF NOT EXISTS "tx_hash" text;--> statement-breakpoint
|
|
4
|
+
ALTER TABLE "crypto_charges" ADD COLUMN IF NOT EXISTS "amount_received_cents" integer DEFAULT 0 NOT NULL;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
ALTER TABLE "payment_methods" ADD COLUMN "encoding_params" text DEFAULT '{}' NOT NULL;
|
|
1
|
+
ALTER TABLE "payment_methods" ADD COLUMN IF NOT EXISTS "encoding_params" text DEFAULT '{}' NOT NULL;
|
|
2
2
|
--> statement-breakpoint
|
|
3
3
|
UPDATE "payment_methods" SET "encoding_params" = '{"hrp":"bc"}' WHERE "address_type" = 'bech32' AND "chain" = 'bitcoin';
|
|
4
4
|
--> statement-breakpoint
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
ALTER TABLE "payment_methods" ADD COLUMN "watcher_type" text DEFAULT 'evm' NOT NULL;
|
|
1
|
+
ALTER TABLE "payment_methods" ADD COLUMN IF NOT EXISTS "watcher_type" text DEFAULT 'evm' NOT NULL;
|
|
2
2
|
--> statement-breakpoint
|
|
3
3
|
UPDATE "payment_methods" SET "watcher_type" = 'utxo' WHERE "chain" IN ('bitcoin', 'litecoin', 'dogecoin');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
ALTER TABLE "payment_methods" ADD COLUMN "oracle_asset_id" text;
|
|
1
|
+
ALTER TABLE "payment_methods" ADD COLUMN IF NOT EXISTS "oracle_asset_id" text;
|
|
2
2
|
--> statement-breakpoint
|
|
3
3
|
UPDATE "payment_methods" SET "oracle_asset_id" = 'bitcoin' WHERE "token" = 'BTC';
|
|
4
4
|
--> statement-breakpoint
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import type { ICryptoChargeRepository } from "../charge-store.js";
|
|
|
3
3
|
import type { KeyServerDeps } from "../key-server.js";
|
|
4
4
|
import { createKeyServerApp } from "../key-server.js";
|
|
5
5
|
import type { IPaymentMethodStore } from "../payment-method-store.js";
|
|
6
|
+
import type { PluginRegistry } from "../plugin/registry.js";
|
|
6
7
|
|
|
7
8
|
/** Create a mock db that supports transaction() by passing itself to the callback. */
|
|
8
9
|
function createMockDb() {
|
|
@@ -350,6 +351,433 @@ describe("key-server routes", () => {
|
|
|
350
351
|
});
|
|
351
352
|
});
|
|
352
353
|
|
|
354
|
+
describe("key-server pool endpoints", () => {
|
|
355
|
+
/** Create a mock db that supports pool queries. */
|
|
356
|
+
function createPoolMockDb(opts?: {
|
|
357
|
+
keyRing?: { id: string; derivationMode: string } | null;
|
|
358
|
+
poolEntries?: Array<{
|
|
359
|
+
id: number;
|
|
360
|
+
keyRingId: string;
|
|
361
|
+
derivationIndex: number;
|
|
362
|
+
publicKey: string;
|
|
363
|
+
address: string;
|
|
364
|
+
assignedTo: string | null;
|
|
365
|
+
}>;
|
|
366
|
+
allKeyRings?: Array<{ id: string }>;
|
|
367
|
+
}) {
|
|
368
|
+
const keyRing = opts?.keyRing ?? null;
|
|
369
|
+
const poolEntries = opts?.poolEntries ?? [];
|
|
370
|
+
const allKeyRings = opts?.allKeyRings ?? (keyRing ? [keyRing] : []);
|
|
371
|
+
|
|
372
|
+
const db: Record<string, unknown> = {};
|
|
373
|
+
|
|
374
|
+
// Track which table is being queried via from()
|
|
375
|
+
db.select = vi.fn().mockReturnValue({
|
|
376
|
+
from: vi.fn().mockImplementation((table: unknown) => {
|
|
377
|
+
const tableName = (table as Record<symbol, string>)[Symbol.for("drizzle:Name")];
|
|
378
|
+
if (tableName === "key_rings") {
|
|
379
|
+
return {
|
|
380
|
+
where: vi.fn().mockResolvedValue(keyRing ? [keyRing] : []),
|
|
381
|
+
orderBy: vi.fn().mockResolvedValue(allKeyRings),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (tableName === "address_pool") {
|
|
385
|
+
return {
|
|
386
|
+
where: vi.fn().mockImplementation(() => ({
|
|
387
|
+
orderBy: vi.fn().mockImplementation(() => ({
|
|
388
|
+
limit: vi.fn().mockResolvedValue(poolEntries.filter((e) => e.assignedTo === null).slice(0, 1)),
|
|
389
|
+
})),
|
|
390
|
+
// For counting all pool entries for a key ring
|
|
391
|
+
length: poolEntries.length,
|
|
392
|
+
filter: (fn: (e: unknown) => boolean) => poolEntries.filter(fn),
|
|
393
|
+
[Symbol.iterator]: function* () {
|
|
394
|
+
yield* poolEntries;
|
|
395
|
+
},
|
|
396
|
+
})),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
// Default: payment_methods
|
|
400
|
+
return {
|
|
401
|
+
where: vi.fn().mockResolvedValue([]),
|
|
402
|
+
};
|
|
403
|
+
}),
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
db.insert = vi.fn().mockReturnValue({
|
|
407
|
+
values: vi.fn().mockReturnValue({
|
|
408
|
+
onConflictDoNothing: vi.fn().mockResolvedValue({ rowCount: 1 }),
|
|
409
|
+
}),
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
db.update = vi.fn().mockReturnValue({
|
|
413
|
+
set: vi.fn().mockReturnValue({
|
|
414
|
+
where: vi.fn().mockReturnValue({
|
|
415
|
+
returning: vi.fn().mockResolvedValue([]),
|
|
416
|
+
}),
|
|
417
|
+
}),
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
db.transaction = vi.fn().mockImplementation(async (fn: (tx: unknown) => unknown) => fn(db));
|
|
421
|
+
|
|
422
|
+
return db;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
it("POST /admin/pool/replenish validates required fields", async () => {
|
|
426
|
+
const deps = mockDeps();
|
|
427
|
+
deps.adminToken = "test-admin";
|
|
428
|
+
const app = createKeyServerApp(deps);
|
|
429
|
+
const res = await app.request("/admin/pool/replenish", {
|
|
430
|
+
method: "POST",
|
|
431
|
+
headers: { "Content-Type": "application/json", Authorization: "Bearer test-admin" },
|
|
432
|
+
body: JSON.stringify({}),
|
|
433
|
+
});
|
|
434
|
+
expect(res.status).toBe(400);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("POST /admin/pool/replenish returns 404 for unknown key ring", async () => {
|
|
438
|
+
const db = createPoolMockDb({ keyRing: null });
|
|
439
|
+
const deps = mockDeps();
|
|
440
|
+
deps.adminToken = "test-admin";
|
|
441
|
+
(deps as unknown as { db: unknown }).db = db;
|
|
442
|
+
const app = createKeyServerApp(deps);
|
|
443
|
+
const res = await app.request("/admin/pool/replenish", {
|
|
444
|
+
method: "POST",
|
|
445
|
+
headers: { "Content-Type": "application/json", Authorization: "Bearer test-admin" },
|
|
446
|
+
body: JSON.stringify({
|
|
447
|
+
key_ring_id: "sol-main",
|
|
448
|
+
plugin_id: "solana",
|
|
449
|
+
encoding: "base58-solana",
|
|
450
|
+
addresses: [{ index: 0, public_key: "abcd", address: "SolAddr1" }],
|
|
451
|
+
}),
|
|
452
|
+
});
|
|
453
|
+
expect(res.status).toBe(404);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("POST /admin/pool/replenish inserts validated addresses", async () => {
|
|
457
|
+
const db = createPoolMockDb({
|
|
458
|
+
keyRing: { id: "sol-main", derivationMode: "pre-derived" },
|
|
459
|
+
poolEntries: [],
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Override select().from() to handle both keyRings query and the count query
|
|
463
|
+
const selectMock = vi.fn().mockReturnValue({
|
|
464
|
+
from: vi.fn().mockImplementation((table: unknown) => {
|
|
465
|
+
const tableName = (table as Record<symbol, string>)[Symbol.for("drizzle:Name")];
|
|
466
|
+
if (tableName === "key_rings") {
|
|
467
|
+
return {
|
|
468
|
+
where: vi.fn().mockResolvedValue([{ id: "sol-main", derivationMode: "pre-derived" }]),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
// address_pool count query (after insert)
|
|
472
|
+
return {
|
|
473
|
+
where: vi.fn().mockResolvedValue([
|
|
474
|
+
{
|
|
475
|
+
id: 1,
|
|
476
|
+
keyRingId: "sol-main",
|
|
477
|
+
derivationIndex: 0,
|
|
478
|
+
publicKey: "ab",
|
|
479
|
+
address: "SolAddr0",
|
|
480
|
+
assignedTo: null,
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
id: 2,
|
|
484
|
+
keyRingId: "sol-main",
|
|
485
|
+
derivationIndex: 1,
|
|
486
|
+
publicKey: "cd",
|
|
487
|
+
address: "SolAddr1",
|
|
488
|
+
assignedTo: null,
|
|
489
|
+
},
|
|
490
|
+
]),
|
|
491
|
+
};
|
|
492
|
+
}),
|
|
493
|
+
});
|
|
494
|
+
(db as Record<string, unknown>).select = selectMock;
|
|
495
|
+
|
|
496
|
+
const deps = mockDeps();
|
|
497
|
+
deps.adminToken = "test-admin";
|
|
498
|
+
(deps as unknown as { db: unknown }).db = db;
|
|
499
|
+
// No registry — skip re-encoding validation
|
|
500
|
+
deps.registry = undefined;
|
|
501
|
+
|
|
502
|
+
const app = createKeyServerApp(deps);
|
|
503
|
+
const res = await app.request("/admin/pool/replenish", {
|
|
504
|
+
method: "POST",
|
|
505
|
+
headers: { "Content-Type": "application/json", Authorization: "Bearer test-admin" },
|
|
506
|
+
body: JSON.stringify({
|
|
507
|
+
key_ring_id: "sol-main",
|
|
508
|
+
plugin_id: "solana",
|
|
509
|
+
encoding: "base58-solana",
|
|
510
|
+
addresses: [
|
|
511
|
+
{ index: 0, public_key: "ab", address: "SolAddr0" },
|
|
512
|
+
{ index: 1, public_key: "cd", address: "SolAddr1" },
|
|
513
|
+
],
|
|
514
|
+
}),
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
expect(res.status).toBe(201);
|
|
518
|
+
const body = await res.json();
|
|
519
|
+
expect(body.inserted).toBe(2);
|
|
520
|
+
expect(body.total).toBe(2);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("POST /admin/pool/replenish rejects mismatched address when encoder present", async () => {
|
|
524
|
+
const mockEncoder = {
|
|
525
|
+
encode: vi.fn().mockReturnValue("CorrectAddress"),
|
|
526
|
+
encodingType: vi.fn().mockReturnValue("base58-solana"),
|
|
527
|
+
};
|
|
528
|
+
const mockPlugin = {
|
|
529
|
+
pluginId: "solana",
|
|
530
|
+
supportedCurve: "ed25519" as const,
|
|
531
|
+
encoders: { "base58-solana": mockEncoder },
|
|
532
|
+
createWatcher: vi.fn(),
|
|
533
|
+
createSweeper: vi.fn(),
|
|
534
|
+
version: 1,
|
|
535
|
+
};
|
|
536
|
+
const mockRegistry = {
|
|
537
|
+
get: vi.fn().mockReturnValue(mockPlugin),
|
|
538
|
+
getOrThrow: vi.fn(),
|
|
539
|
+
list: vi.fn(),
|
|
540
|
+
register: vi.fn(),
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const db = createPoolMockDb({
|
|
544
|
+
keyRing: { id: "sol-main", derivationMode: "pre-derived" },
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const deps = mockDeps();
|
|
548
|
+
deps.adminToken = "test-admin";
|
|
549
|
+
(deps as unknown as { db: unknown }).db = db;
|
|
550
|
+
deps.registry = mockRegistry as unknown as PluginRegistry;
|
|
551
|
+
|
|
552
|
+
const app = createKeyServerApp(deps);
|
|
553
|
+
const res = await app.request("/admin/pool/replenish", {
|
|
554
|
+
method: "POST",
|
|
555
|
+
headers: { "Content-Type": "application/json", Authorization: "Bearer test-admin" },
|
|
556
|
+
body: JSON.stringify({
|
|
557
|
+
key_ring_id: "sol-main",
|
|
558
|
+
plugin_id: "solana",
|
|
559
|
+
encoding: "base58-solana",
|
|
560
|
+
addresses: [{ index: 0, public_key: "abcd1234", address: "WrongAddress" }],
|
|
561
|
+
}),
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
expect(res.status).toBe(400);
|
|
565
|
+
const body = await res.json();
|
|
566
|
+
expect(body.error).toContain("Address mismatch");
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it("GET /admin/pool/status returns pool stats", async () => {
|
|
570
|
+
const db = createPoolMockDb({
|
|
571
|
+
allKeyRings: [{ id: "sol-main" }],
|
|
572
|
+
poolEntries: [
|
|
573
|
+
{ id: 1, keyRingId: "sol-main", derivationIndex: 0, publicKey: "a", address: "A", assignedTo: null },
|
|
574
|
+
{ id: 2, keyRingId: "sol-main", derivationIndex: 1, publicKey: "b", address: "B", assignedTo: "tenant:1" },
|
|
575
|
+
{ id: 3, keyRingId: "sol-main", derivationIndex: 2, publicKey: "c", address: "C", assignedTo: null },
|
|
576
|
+
],
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Override select to handle the two different query patterns in pool/status
|
|
580
|
+
let selectCallCount = 0;
|
|
581
|
+
const poolEntries = [
|
|
582
|
+
{ id: 1, keyRingId: "sol-main", derivationIndex: 0, publicKey: "a", address: "A", assignedTo: null },
|
|
583
|
+
{ id: 2, keyRingId: "sol-main", derivationIndex: 1, publicKey: "b", address: "B", assignedTo: "tenant:1" },
|
|
584
|
+
{ id: 3, keyRingId: "sol-main", derivationIndex: 2, publicKey: "c", address: "C", assignedTo: null },
|
|
585
|
+
];
|
|
586
|
+
(db as Record<string, unknown>).select = vi.fn().mockReturnValue({
|
|
587
|
+
from: vi.fn().mockImplementation(() => {
|
|
588
|
+
selectCallCount++;
|
|
589
|
+
if (selectCallCount === 1) {
|
|
590
|
+
// First call: select from keyRings (no where clause)
|
|
591
|
+
return [{ id: "sol-main" }];
|
|
592
|
+
}
|
|
593
|
+
// Second call: select from addressPool where keyRingId = ring.id
|
|
594
|
+
return {
|
|
595
|
+
where: vi.fn().mockResolvedValue(poolEntries),
|
|
596
|
+
};
|
|
597
|
+
}),
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
const deps = mockDeps();
|
|
601
|
+
deps.adminToken = "test-admin";
|
|
602
|
+
(deps as unknown as { db: unknown }).db = db;
|
|
603
|
+
const app = createKeyServerApp(deps);
|
|
604
|
+
|
|
605
|
+
const res = await app.request("/admin/pool/status", {
|
|
606
|
+
headers: { Authorization: "Bearer test-admin" },
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
expect(res.status).toBe(200);
|
|
610
|
+
const body = await res.json();
|
|
611
|
+
expect(body.pools).toHaveLength(1);
|
|
612
|
+
expect(body.pools[0].key_ring_id).toBe("sol-main");
|
|
613
|
+
expect(body.pools[0].total).toBe(3);
|
|
614
|
+
expect(body.pools[0].available).toBe(2);
|
|
615
|
+
expect(body.pools[0].assigned).toBe(1);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("POST /address uses pool for pre-derived key ring", async () => {
|
|
619
|
+
const poolEntry = {
|
|
620
|
+
id: 1,
|
|
621
|
+
keyRingId: "sol-main",
|
|
622
|
+
derivationIndex: 7,
|
|
623
|
+
publicKey: "deadbeef",
|
|
624
|
+
address: "SolanaAddr7",
|
|
625
|
+
assignedTo: null,
|
|
626
|
+
createdAt: "2026-01-01",
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
const solMethod = {
|
|
630
|
+
id: "sol",
|
|
631
|
+
type: "native",
|
|
632
|
+
token: "SOL",
|
|
633
|
+
chain: "solana",
|
|
634
|
+
xpub: null,
|
|
635
|
+
keyRingId: "sol-main",
|
|
636
|
+
nextIndex: 0,
|
|
637
|
+
decimals: 9,
|
|
638
|
+
addressType: "base58-solana",
|
|
639
|
+
encodingParams: "{}",
|
|
640
|
+
watcherType: "solana",
|
|
641
|
+
oracleAssetId: "solana",
|
|
642
|
+
confirmations: 1,
|
|
643
|
+
pluginId: "solana",
|
|
644
|
+
encoding: "base58-solana",
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const keyRing = {
|
|
648
|
+
id: "sol-main",
|
|
649
|
+
curve: "ed25519",
|
|
650
|
+
derivationScheme: "slip10",
|
|
651
|
+
derivationMode: "pre-derived",
|
|
652
|
+
keyMaterial: "{}",
|
|
653
|
+
coinType: 501,
|
|
654
|
+
accountIndex: 0,
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// Build a mock that handles the pool flow:
|
|
658
|
+
// 1. select from paymentMethods where id=sol -> solMethod
|
|
659
|
+
// 2. select from keyRings where id=sol-main -> keyRing
|
|
660
|
+
// 3. transaction: select from addressPool -> poolEntry, update addressPool, insert derivedAddresses
|
|
661
|
+
let selectCallCount = 0;
|
|
662
|
+
const db = {
|
|
663
|
+
select: vi.fn().mockImplementation(() => ({
|
|
664
|
+
from: vi.fn().mockImplementation(() => {
|
|
665
|
+
selectCallCount++;
|
|
666
|
+
if (selectCallCount === 1) {
|
|
667
|
+
// paymentMethods lookup
|
|
668
|
+
return { where: vi.fn().mockResolvedValue([solMethod]) };
|
|
669
|
+
}
|
|
670
|
+
if (selectCallCount === 2) {
|
|
671
|
+
// keyRings lookup
|
|
672
|
+
return { where: vi.fn().mockResolvedValue([keyRing]) };
|
|
673
|
+
}
|
|
674
|
+
// addressPool query inside transaction
|
|
675
|
+
return {
|
|
676
|
+
where: vi.fn().mockReturnValue({
|
|
677
|
+
orderBy: vi.fn().mockReturnValue({
|
|
678
|
+
limit: vi.fn().mockResolvedValue([poolEntry]),
|
|
679
|
+
}),
|
|
680
|
+
}),
|
|
681
|
+
};
|
|
682
|
+
}),
|
|
683
|
+
})),
|
|
684
|
+
update: vi.fn().mockReturnValue({
|
|
685
|
+
set: vi.fn().mockReturnValue({
|
|
686
|
+
where: vi.fn().mockResolvedValue(undefined),
|
|
687
|
+
}),
|
|
688
|
+
}),
|
|
689
|
+
insert: vi.fn().mockReturnValue({
|
|
690
|
+
values: vi.fn().mockResolvedValue(undefined),
|
|
691
|
+
}),
|
|
692
|
+
transaction: vi.fn().mockImplementation(async (fn: (tx: unknown) => unknown) => fn(db)),
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
const deps = mockDeps();
|
|
696
|
+
(deps as unknown as { db: unknown }).db = db;
|
|
697
|
+
const app = createKeyServerApp(deps);
|
|
698
|
+
|
|
699
|
+
const res = await app.request("/address", {
|
|
700
|
+
method: "POST",
|
|
701
|
+
headers: { "Content-Type": "application/json" },
|
|
702
|
+
body: JSON.stringify({ chain: "sol" }),
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
expect(res.status).toBe(201);
|
|
706
|
+
const body = await res.json();
|
|
707
|
+
expect(body.address).toBe("SolanaAddr7");
|
|
708
|
+
expect(body.index).toBe(7);
|
|
709
|
+
expect(body.chain).toBe("solana");
|
|
710
|
+
expect(body.token).toBe("SOL");
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it("POST /address throws when pool is empty", async () => {
|
|
714
|
+
const solMethod = {
|
|
715
|
+
id: "sol",
|
|
716
|
+
type: "native",
|
|
717
|
+
token: "SOL",
|
|
718
|
+
chain: "solana",
|
|
719
|
+
xpub: null,
|
|
720
|
+
keyRingId: "sol-main",
|
|
721
|
+
nextIndex: 0,
|
|
722
|
+
decimals: 9,
|
|
723
|
+
addressType: "base58-solana",
|
|
724
|
+
encodingParams: "{}",
|
|
725
|
+
watcherType: "solana",
|
|
726
|
+
oracleAssetId: "solana",
|
|
727
|
+
confirmations: 1,
|
|
728
|
+
pluginId: "solana",
|
|
729
|
+
encoding: "base58-solana",
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
const keyRing = {
|
|
733
|
+
id: "sol-main",
|
|
734
|
+
curve: "ed25519",
|
|
735
|
+
derivationScheme: "slip10",
|
|
736
|
+
derivationMode: "pre-derived",
|
|
737
|
+
keyMaterial: "{}",
|
|
738
|
+
coinType: 501,
|
|
739
|
+
accountIndex: 0,
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
let selectCallCount = 0;
|
|
743
|
+
const db = {
|
|
744
|
+
select: vi.fn().mockImplementation(() => ({
|
|
745
|
+
from: vi.fn().mockImplementation(() => {
|
|
746
|
+
selectCallCount++;
|
|
747
|
+
if (selectCallCount === 1) {
|
|
748
|
+
return { where: vi.fn().mockResolvedValue([solMethod]) };
|
|
749
|
+
}
|
|
750
|
+
if (selectCallCount === 2) {
|
|
751
|
+
return { where: vi.fn().mockResolvedValue([keyRing]) };
|
|
752
|
+
}
|
|
753
|
+
// Empty pool
|
|
754
|
+
return {
|
|
755
|
+
where: vi.fn().mockReturnValue({
|
|
756
|
+
orderBy: vi.fn().mockReturnValue({
|
|
757
|
+
limit: vi.fn().mockResolvedValue([]),
|
|
758
|
+
}),
|
|
759
|
+
}),
|
|
760
|
+
};
|
|
761
|
+
}),
|
|
762
|
+
})),
|
|
763
|
+
transaction: vi.fn().mockImplementation(async (fn: (tx: unknown) => unknown) => fn(db)),
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
const deps = mockDeps();
|
|
767
|
+
(deps as unknown as { db: unknown }).db = db;
|
|
768
|
+
const app = createKeyServerApp(deps);
|
|
769
|
+
|
|
770
|
+
const res = await app.request("/address", {
|
|
771
|
+
method: "POST",
|
|
772
|
+
headers: { "Content-Type": "application/json" },
|
|
773
|
+
body: JSON.stringify({ chain: "sol" }),
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// Hono catches the thrown error and returns 500
|
|
777
|
+
expect(res.status).toBe(500);
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
|
|
353
781
|
describe("key-server auth", () => {
|
|
354
782
|
it("rejects unauthenticated request when serviceKey is set", async () => {
|
|
355
783
|
const deps = mockDeps();
|