@wopr-network/platform-core 1.42.3 → 1.44.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.
Files changed (63) hide show
  1. package/.github/workflows/key-server-image.yml +35 -0
  2. package/Dockerfile.key-server +20 -0
  3. package/GATEWAY_BILLING_RESEARCH.md +430 -0
  4. package/biome.json +2 -9
  5. package/dist/billing/crypto/__tests__/key-server.test.js +240 -0
  6. package/dist/billing/crypto/btc/watcher.d.ts +2 -0
  7. package/dist/billing/crypto/btc/watcher.js +1 -1
  8. package/dist/billing/crypto/charge-store.d.ts +7 -1
  9. package/dist/billing/crypto/charge-store.js +7 -1
  10. package/dist/billing/crypto/client.d.ts +68 -30
  11. package/dist/billing/crypto/client.js +63 -46
  12. package/dist/billing/crypto/client.test.js +66 -83
  13. package/dist/billing/crypto/index.d.ts +8 -8
  14. package/dist/billing/crypto/index.js +4 -5
  15. package/dist/billing/crypto/key-server-entry.js +84 -0
  16. package/dist/billing/crypto/key-server-webhook.d.ts +33 -0
  17. package/dist/billing/crypto/key-server-webhook.js +73 -0
  18. package/dist/billing/crypto/key-server.d.ts +20 -0
  19. package/dist/billing/crypto/key-server.js +263 -0
  20. package/dist/billing/crypto/watcher-service.d.ts +33 -0
  21. package/dist/billing/crypto/watcher-service.js +295 -0
  22. package/dist/billing/index.js +1 -1
  23. package/dist/db/schema/crypto.d.ts +464 -2
  24. package/dist/db/schema/crypto.js +60 -6
  25. package/dist/monetization/crypto/__tests__/webhook.test.js +57 -92
  26. package/dist/monetization/crypto/index.d.ts +4 -4
  27. package/dist/monetization/crypto/index.js +2 -2
  28. package/dist/monetization/crypto/webhook.d.ts +13 -14
  29. package/dist/monetization/crypto/webhook.js +12 -83
  30. package/dist/monetization/index.d.ts +2 -2
  31. package/dist/monetization/index.js +1 -1
  32. package/drizzle/migrations/0014_crypto_key_server.sql +60 -0
  33. package/drizzle/migrations/0015_callback_url.sql +32 -0
  34. package/drizzle/migrations/meta/_journal.json +28 -0
  35. package/package.json +2 -1
  36. package/src/billing/crypto/__tests__/key-server.test.ts +262 -0
  37. package/src/billing/crypto/btc/watcher.ts +3 -1
  38. package/src/billing/crypto/charge-store.ts +13 -1
  39. package/src/billing/crypto/client.test.ts +70 -98
  40. package/src/billing/crypto/client.ts +118 -59
  41. package/src/billing/crypto/index.ts +19 -14
  42. package/src/billing/crypto/key-server-entry.ts +96 -0
  43. package/src/billing/crypto/key-server-webhook.ts +119 -0
  44. package/src/billing/crypto/key-server.ts +343 -0
  45. package/src/billing/crypto/watcher-service.ts +381 -0
  46. package/src/billing/index.ts +1 -1
  47. package/src/db/schema/crypto.ts +75 -6
  48. package/src/monetization/crypto/__tests__/webhook.test.ts +61 -104
  49. package/src/monetization/crypto/index.ts +9 -11
  50. package/src/monetization/crypto/webhook.ts +25 -99
  51. package/src/monetization/index.ts +3 -7
  52. package/dist/billing/crypto/checkout.d.ts +0 -18
  53. package/dist/billing/crypto/checkout.js +0 -35
  54. package/dist/billing/crypto/checkout.test.js +0 -71
  55. package/dist/billing/crypto/webhook.d.ts +0 -34
  56. package/dist/billing/crypto/webhook.js +0 -107
  57. package/dist/billing/crypto/webhook.test.js +0 -266
  58. package/src/billing/crypto/checkout.test.ts +0 -93
  59. package/src/billing/crypto/checkout.ts +0 -48
  60. package/src/billing/crypto/webhook.test.ts +0 -340
  61. package/src/billing/crypto/webhook.ts +0 -136
  62. /package/dist/billing/crypto/{checkout.test.d.ts → __tests__/key-server.test.d.ts} +0 -0
  63. /package/dist/billing/crypto/{webhook.test.d.ts → key-server-entry.d.ts} +0 -0
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Crypto payment charges — tracks the lifecycle of each BTCPay invoice.
3
- * reference_id is the BTCPay invoice ID.
2
+ * Crypto payment charges — tracks the lifecycle of each payment.
3
+ * reference_id is the charge ID (e.g. "btc:bc1q...").
4
4
  *
5
5
  * amountUsdCents stores the requested amount in USD cents (integer).
6
6
  * This is NOT nanodollars — Credit.fromCents() handles the conversion
@@ -231,6 +231,57 @@ export declare const cryptoCharges: import("drizzle-orm/pg-core").PgTableWithCol
231
231
  identity: undefined;
232
232
  generated: undefined;
233
233
  }, {}, {}>;
234
+ callbackUrl: import("drizzle-orm/pg-core").PgColumn<{
235
+ name: "callback_url";
236
+ tableName: "crypto_charges";
237
+ dataType: "string";
238
+ columnType: "PgText";
239
+ data: string;
240
+ driverParam: string;
241
+ notNull: false;
242
+ hasDefault: false;
243
+ isPrimaryKey: false;
244
+ isAutoincrement: false;
245
+ hasRuntimeDefault: false;
246
+ enumValues: [string, ...string[]];
247
+ baseColumn: never;
248
+ identity: undefined;
249
+ generated: undefined;
250
+ }, {}, {}>;
251
+ expectedAmount: import("drizzle-orm/pg-core").PgColumn<{
252
+ name: "expected_amount";
253
+ tableName: "crypto_charges";
254
+ dataType: "string";
255
+ columnType: "PgText";
256
+ data: string;
257
+ driverParam: string;
258
+ notNull: false;
259
+ hasDefault: false;
260
+ isPrimaryKey: false;
261
+ isAutoincrement: false;
262
+ hasRuntimeDefault: false;
263
+ enumValues: [string, ...string[]];
264
+ baseColumn: never;
265
+ identity: undefined;
266
+ generated: undefined;
267
+ }, {}, {}>;
268
+ receivedAmount: import("drizzle-orm/pg-core").PgColumn<{
269
+ name: "received_amount";
270
+ tableName: "crypto_charges";
271
+ dataType: "string";
272
+ columnType: "PgText";
273
+ data: string;
274
+ driverParam: string;
275
+ notNull: false;
276
+ hasDefault: false;
277
+ isPrimaryKey: false;
278
+ isAutoincrement: false;
279
+ hasRuntimeDefault: false;
280
+ enumValues: [string, ...string[]];
281
+ baseColumn: never;
282
+ identity: undefined;
283
+ generated: undefined;
284
+ }, {}, {}>;
234
285
  };
235
286
  dialect: "pg";
236
287
  }>;
@@ -300,6 +351,9 @@ export declare const watcherCursors: import("drizzle-orm/pg-core").PgTableWithCo
300
351
  * Payment method registry — runtime-configurable tokens/chains.
301
352
  * Admin inserts a row to enable a new payment method. No deploy needed.
302
353
  * Contract addresses are immutable on-chain but configurable here.
354
+ *
355
+ * nextIndex is an atomic counter for HD derivation — never reuses an index.
356
+ * Increment via UPDATE ... SET next_index = next_index + 1 RETURNING next_index.
303
357
  */
304
358
  export declare const paymentMethods: import("drizzle-orm/pg-core").PgTableWithColumns<{
305
359
  name: "payment_methods";
@@ -373,6 +427,23 @@ export declare const paymentMethods: import("drizzle-orm/pg-core").PgTableWithCo
373
427
  identity: undefined;
374
428
  generated: undefined;
375
429
  }, {}, {}>;
430
+ network: import("drizzle-orm/pg-core").PgColumn<{
431
+ name: "network";
432
+ tableName: "payment_methods";
433
+ dataType: "string";
434
+ columnType: "PgText";
435
+ data: string;
436
+ driverParam: string;
437
+ notNull: true;
438
+ hasDefault: true;
439
+ isPrimaryKey: false;
440
+ isAutoincrement: false;
441
+ hasRuntimeDefault: false;
442
+ enumValues: [string, ...string[]];
443
+ baseColumn: never;
444
+ identity: undefined;
445
+ generated: undefined;
446
+ }, {}, {}>;
376
447
  contractAddress: import("drizzle-orm/pg-core").PgColumn<{
377
448
  name: "contract_address";
378
449
  tableName: "payment_methods";
@@ -526,6 +597,23 @@ export declare const paymentMethods: import("drizzle-orm/pg-core").PgTableWithCo
526
597
  identity: undefined;
527
598
  generated: undefined;
528
599
  }, {}, {}>;
600
+ nextIndex: import("drizzle-orm/pg-core").PgColumn<{
601
+ name: "next_index";
602
+ tableName: "payment_methods";
603
+ dataType: "number";
604
+ columnType: "PgInteger";
605
+ data: number;
606
+ driverParam: string | number;
607
+ notNull: true;
608
+ hasDefault: true;
609
+ isPrimaryKey: false;
610
+ isAutoincrement: false;
611
+ hasRuntimeDefault: false;
612
+ enumValues: undefined;
613
+ baseColumn: never;
614
+ identity: undefined;
615
+ generated: undefined;
616
+ }, {}, {}>;
529
617
  createdAt: import("drizzle-orm/pg-core").PgColumn<{
530
618
  name: "created_at";
531
619
  tableName: "payment_methods";
@@ -546,6 +634,380 @@ export declare const paymentMethods: import("drizzle-orm/pg-core").PgTableWithCo
546
634
  };
547
635
  dialect: "pg";
548
636
  }>;
637
+ /**
638
+ * BIP-44 path allocation registry — tracks which derivation paths are in use.
639
+ * The server knows which paths are allocated so you never collide.
640
+ * The seed phrase never touches the server — only xpubs.
641
+ */
642
+ export declare const pathAllocations: import("drizzle-orm/pg-core").PgTableWithColumns<{
643
+ name: "path_allocations";
644
+ schema: undefined;
645
+ columns: {
646
+ coinType: import("drizzle-orm/pg-core").PgColumn<{
647
+ name: "coin_type";
648
+ tableName: "path_allocations";
649
+ dataType: "number";
650
+ columnType: "PgInteger";
651
+ data: number;
652
+ driverParam: string | number;
653
+ notNull: true;
654
+ hasDefault: false;
655
+ isPrimaryKey: false;
656
+ isAutoincrement: false;
657
+ hasRuntimeDefault: false;
658
+ enumValues: undefined;
659
+ baseColumn: never;
660
+ identity: undefined;
661
+ generated: undefined;
662
+ }, {}, {}>;
663
+ accountIndex: import("drizzle-orm/pg-core").PgColumn<{
664
+ name: "account_index";
665
+ tableName: "path_allocations";
666
+ dataType: "number";
667
+ columnType: "PgInteger";
668
+ data: number;
669
+ driverParam: string | number;
670
+ notNull: true;
671
+ hasDefault: false;
672
+ isPrimaryKey: false;
673
+ isAutoincrement: false;
674
+ hasRuntimeDefault: false;
675
+ enumValues: undefined;
676
+ baseColumn: never;
677
+ identity: undefined;
678
+ generated: undefined;
679
+ }, {}, {}>;
680
+ chainId: import("drizzle-orm/pg-core").PgColumn<{
681
+ name: "chain_id";
682
+ tableName: "path_allocations";
683
+ dataType: "string";
684
+ columnType: "PgText";
685
+ data: string;
686
+ driverParam: string;
687
+ notNull: false;
688
+ hasDefault: false;
689
+ isPrimaryKey: false;
690
+ isAutoincrement: false;
691
+ hasRuntimeDefault: false;
692
+ enumValues: [string, ...string[]];
693
+ baseColumn: never;
694
+ identity: undefined;
695
+ generated: undefined;
696
+ }, {}, {}>;
697
+ xpub: import("drizzle-orm/pg-core").PgColumn<{
698
+ name: "xpub";
699
+ tableName: "path_allocations";
700
+ dataType: "string";
701
+ columnType: "PgText";
702
+ data: string;
703
+ driverParam: string;
704
+ notNull: true;
705
+ hasDefault: false;
706
+ isPrimaryKey: false;
707
+ isAutoincrement: false;
708
+ hasRuntimeDefault: false;
709
+ enumValues: [string, ...string[]];
710
+ baseColumn: never;
711
+ identity: undefined;
712
+ generated: undefined;
713
+ }, {}, {}>;
714
+ allocatedAt: import("drizzle-orm/pg-core").PgColumn<{
715
+ name: "allocated_at";
716
+ tableName: "path_allocations";
717
+ dataType: "string";
718
+ columnType: "PgText";
719
+ data: string;
720
+ driverParam: string;
721
+ notNull: true;
722
+ hasDefault: true;
723
+ isPrimaryKey: false;
724
+ isAutoincrement: false;
725
+ hasRuntimeDefault: false;
726
+ enumValues: [string, ...string[]];
727
+ baseColumn: never;
728
+ identity: undefined;
729
+ generated: undefined;
730
+ }, {}, {}>;
731
+ };
732
+ dialect: "pg";
733
+ }>;
734
+ /**
735
+ * Webhook delivery outbox — durable retry for payment callbacks.
736
+ * Inserted when a payment is confirmed. Retried until the receiver ACKs.
737
+ */
738
+ export declare const webhookDeliveries: import("drizzle-orm/pg-core").PgTableWithColumns<{
739
+ name: "webhook_deliveries";
740
+ schema: undefined;
741
+ columns: {
742
+ id: import("drizzle-orm/pg-core").PgColumn<{
743
+ name: "id";
744
+ tableName: "webhook_deliveries";
745
+ dataType: "number";
746
+ columnType: "PgInteger";
747
+ data: number;
748
+ driverParam: string | number;
749
+ notNull: true;
750
+ hasDefault: true;
751
+ isPrimaryKey: true;
752
+ isAutoincrement: false;
753
+ hasRuntimeDefault: false;
754
+ enumValues: undefined;
755
+ baseColumn: never;
756
+ identity: "always";
757
+ generated: undefined;
758
+ }, {}, {}>;
759
+ chargeId: import("drizzle-orm/pg-core").PgColumn<{
760
+ name: "charge_id";
761
+ tableName: "webhook_deliveries";
762
+ dataType: "string";
763
+ columnType: "PgText";
764
+ data: string;
765
+ driverParam: string;
766
+ notNull: true;
767
+ hasDefault: false;
768
+ isPrimaryKey: false;
769
+ isAutoincrement: false;
770
+ hasRuntimeDefault: false;
771
+ enumValues: [string, ...string[]];
772
+ baseColumn: never;
773
+ identity: undefined;
774
+ generated: undefined;
775
+ }, {}, {}>;
776
+ callbackUrl: import("drizzle-orm/pg-core").PgColumn<{
777
+ name: "callback_url";
778
+ tableName: "webhook_deliveries";
779
+ dataType: "string";
780
+ columnType: "PgText";
781
+ data: string;
782
+ driverParam: string;
783
+ notNull: true;
784
+ hasDefault: false;
785
+ isPrimaryKey: false;
786
+ isAutoincrement: false;
787
+ hasRuntimeDefault: false;
788
+ enumValues: [string, ...string[]];
789
+ baseColumn: never;
790
+ identity: undefined;
791
+ generated: undefined;
792
+ }, {}, {}>;
793
+ payload: import("drizzle-orm/pg-core").PgColumn<{
794
+ name: "payload";
795
+ tableName: "webhook_deliveries";
796
+ dataType: "string";
797
+ columnType: "PgText";
798
+ data: string;
799
+ driverParam: string;
800
+ notNull: true;
801
+ hasDefault: false;
802
+ isPrimaryKey: false;
803
+ isAutoincrement: false;
804
+ hasRuntimeDefault: false;
805
+ enumValues: [string, ...string[]];
806
+ baseColumn: never;
807
+ identity: undefined;
808
+ generated: undefined;
809
+ }, {}, {}>;
810
+ status: import("drizzle-orm/pg-core").PgColumn<{
811
+ name: "status";
812
+ tableName: "webhook_deliveries";
813
+ dataType: "string";
814
+ columnType: "PgText";
815
+ data: string;
816
+ driverParam: string;
817
+ notNull: true;
818
+ hasDefault: true;
819
+ isPrimaryKey: false;
820
+ isAutoincrement: false;
821
+ hasRuntimeDefault: false;
822
+ enumValues: [string, ...string[]];
823
+ baseColumn: never;
824
+ identity: undefined;
825
+ generated: undefined;
826
+ }, {}, {}>;
827
+ attempts: import("drizzle-orm/pg-core").PgColumn<{
828
+ name: "attempts";
829
+ tableName: "webhook_deliveries";
830
+ dataType: "number";
831
+ columnType: "PgInteger";
832
+ data: number;
833
+ driverParam: string | number;
834
+ notNull: true;
835
+ hasDefault: true;
836
+ isPrimaryKey: false;
837
+ isAutoincrement: false;
838
+ hasRuntimeDefault: false;
839
+ enumValues: undefined;
840
+ baseColumn: never;
841
+ identity: undefined;
842
+ generated: undefined;
843
+ }, {}, {}>;
844
+ nextRetryAt: import("drizzle-orm/pg-core").PgColumn<{
845
+ name: "next_retry_at";
846
+ tableName: "webhook_deliveries";
847
+ dataType: "string";
848
+ columnType: "PgText";
849
+ data: string;
850
+ driverParam: string;
851
+ notNull: false;
852
+ hasDefault: false;
853
+ isPrimaryKey: false;
854
+ isAutoincrement: false;
855
+ hasRuntimeDefault: false;
856
+ enumValues: [string, ...string[]];
857
+ baseColumn: never;
858
+ identity: undefined;
859
+ generated: undefined;
860
+ }, {}, {}>;
861
+ lastError: import("drizzle-orm/pg-core").PgColumn<{
862
+ name: "last_error";
863
+ tableName: "webhook_deliveries";
864
+ dataType: "string";
865
+ columnType: "PgText";
866
+ data: string;
867
+ driverParam: string;
868
+ notNull: false;
869
+ hasDefault: false;
870
+ isPrimaryKey: false;
871
+ isAutoincrement: false;
872
+ hasRuntimeDefault: false;
873
+ enumValues: [string, ...string[]];
874
+ baseColumn: never;
875
+ identity: undefined;
876
+ generated: undefined;
877
+ }, {}, {}>;
878
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
879
+ name: "created_at";
880
+ tableName: "webhook_deliveries";
881
+ dataType: "string";
882
+ columnType: "PgText";
883
+ data: string;
884
+ driverParam: string;
885
+ notNull: true;
886
+ hasDefault: true;
887
+ isPrimaryKey: false;
888
+ isAutoincrement: false;
889
+ hasRuntimeDefault: false;
890
+ enumValues: [string, ...string[]];
891
+ baseColumn: never;
892
+ identity: undefined;
893
+ generated: undefined;
894
+ }, {}, {}>;
895
+ };
896
+ dialect: "pg";
897
+ }>;
898
+ /**
899
+ * Every address ever derived — immutable append-only log.
900
+ * Used for auditing and ensuring no address is ever reused.
901
+ */
902
+ export declare const derivedAddresses: import("drizzle-orm/pg-core").PgTableWithColumns<{
903
+ name: "derived_addresses";
904
+ schema: undefined;
905
+ columns: {
906
+ id: import("drizzle-orm/pg-core").PgColumn<{
907
+ name: "id";
908
+ tableName: "derived_addresses";
909
+ dataType: "number";
910
+ columnType: "PgInteger";
911
+ data: number;
912
+ driverParam: string | number;
913
+ notNull: true;
914
+ hasDefault: true;
915
+ isPrimaryKey: true;
916
+ isAutoincrement: false;
917
+ hasRuntimeDefault: false;
918
+ enumValues: undefined;
919
+ baseColumn: never;
920
+ identity: "always";
921
+ generated: undefined;
922
+ }, {}, {}>;
923
+ chainId: import("drizzle-orm/pg-core").PgColumn<{
924
+ name: "chain_id";
925
+ tableName: "derived_addresses";
926
+ dataType: "string";
927
+ columnType: "PgText";
928
+ data: string;
929
+ driverParam: string;
930
+ notNull: true;
931
+ hasDefault: false;
932
+ isPrimaryKey: false;
933
+ isAutoincrement: false;
934
+ hasRuntimeDefault: false;
935
+ enumValues: [string, ...string[]];
936
+ baseColumn: never;
937
+ identity: undefined;
938
+ generated: undefined;
939
+ }, {}, {}>;
940
+ derivationIndex: import("drizzle-orm/pg-core").PgColumn<{
941
+ name: "derivation_index";
942
+ tableName: "derived_addresses";
943
+ dataType: "number";
944
+ columnType: "PgInteger";
945
+ data: number;
946
+ driverParam: string | number;
947
+ notNull: true;
948
+ hasDefault: false;
949
+ isPrimaryKey: false;
950
+ isAutoincrement: false;
951
+ hasRuntimeDefault: false;
952
+ enumValues: undefined;
953
+ baseColumn: never;
954
+ identity: undefined;
955
+ generated: undefined;
956
+ }, {}, {}>;
957
+ address: import("drizzle-orm/pg-core").PgColumn<{
958
+ name: "address";
959
+ tableName: "derived_addresses";
960
+ dataType: "string";
961
+ columnType: "PgText";
962
+ data: string;
963
+ driverParam: string;
964
+ notNull: true;
965
+ hasDefault: false;
966
+ isPrimaryKey: false;
967
+ isAutoincrement: false;
968
+ hasRuntimeDefault: false;
969
+ enumValues: [string, ...string[]];
970
+ baseColumn: never;
971
+ identity: undefined;
972
+ generated: undefined;
973
+ }, {}, {}>;
974
+ tenantId: import("drizzle-orm/pg-core").PgColumn<{
975
+ name: "tenant_id";
976
+ tableName: "derived_addresses";
977
+ dataType: "string";
978
+ columnType: "PgText";
979
+ data: string;
980
+ driverParam: string;
981
+ notNull: false;
982
+ hasDefault: false;
983
+ isPrimaryKey: false;
984
+ isAutoincrement: false;
985
+ hasRuntimeDefault: false;
986
+ enumValues: [string, ...string[]];
987
+ baseColumn: never;
988
+ identity: undefined;
989
+ generated: undefined;
990
+ }, {}, {}>;
991
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
992
+ name: "created_at";
993
+ tableName: "derived_addresses";
994
+ dataType: "string";
995
+ columnType: "PgText";
996
+ data: string;
997
+ driverParam: string;
998
+ notNull: true;
999
+ hasDefault: true;
1000
+ isPrimaryKey: false;
1001
+ isAutoincrement: false;
1002
+ hasRuntimeDefault: false;
1003
+ enumValues: [string, ...string[]];
1004
+ baseColumn: never;
1005
+ identity: undefined;
1006
+ generated: undefined;
1007
+ }, {}, {}>;
1008
+ };
1009
+ dialect: "pg";
1010
+ }>;
549
1011
  /** Processed transaction IDs for watchers without block cursors (e.g. BTC). */
550
1012
  export declare const watcherProcessed: import("drizzle-orm/pg-core").PgTableWithColumns<{
551
1013
  name: "watcher_processed";
@@ -1,8 +1,8 @@
1
1
  import { sql } from "drizzle-orm";
2
2
  import { boolean, index, integer, pgTable, primaryKey, text } from "drizzle-orm/pg-core";
3
3
  /**
4
- * Crypto payment charges — tracks the lifecycle of each BTCPay invoice.
5
- * reference_id is the BTCPay invoice ID.
4
+ * Crypto payment charges — tracks the lifecycle of each payment.
5
+ * reference_id is the charge ID (e.g. "btc:bc1q...").
6
6
  *
7
7
  * amountUsdCents stores the requested amount in USD cents (integer).
8
8
  * This is NOT nanodollars — Credit.fromCents() handles the conversion
@@ -22,6 +22,11 @@ export const cryptoCharges = pgTable("crypto_charges", {
22
22
  token: text("token"),
23
23
  depositAddress: text("deposit_address"),
24
24
  derivationIndex: integer("derivation_index"),
25
+ callbackUrl: text("callback_url"),
26
+ /** Expected crypto amount in native units (e.g. "76923" sats, "50000000" USDC base units). Locked at creation. */
27
+ expectedAmount: text("expected_amount"),
28
+ /** Running total of received crypto in native units. Accumulates across partial payments. */
29
+ receivedAmount: text("received_amount"),
25
30
  }, (table) => [
26
31
  index("idx_crypto_charges_tenant").on(table.tenantId),
27
32
  index("idx_crypto_charges_status").on(table.status),
@@ -43,12 +48,16 @@ export const watcherCursors = pgTable("watcher_cursors", {
43
48
  * Payment method registry — runtime-configurable tokens/chains.
44
49
  * Admin inserts a row to enable a new payment method. No deploy needed.
45
50
  * Contract addresses are immutable on-chain but configurable here.
51
+ *
52
+ * nextIndex is an atomic counter for HD derivation — never reuses an index.
53
+ * Increment via UPDATE ... SET next_index = next_index + 1 RETURNING next_index.
46
54
  */
47
55
  export const paymentMethods = pgTable("payment_methods", {
48
- id: text("id").primaryKey(), // "USDC:base", "ETH:base", "BTC:mainnet"
49
- type: text("type").notNull(), // "stablecoin", "eth", "btc"
50
- token: text("token").notNull(), // "USDC", "ETH", "BTC"
51
- chain: text("chain").notNull(), // "base", "ethereum", "bitcoin"
56
+ id: text("id").primaryKey(), // "btc", "base-usdc", "arb-usdc", "doge"
57
+ type: text("type").notNull(), // "erc20", "native", "btc"
58
+ token: text("token").notNull(), // "USDC", "ETH", "BTC", "DOGE"
59
+ chain: text("chain").notNull(), // "base", "ethereum", "bitcoin", "arbitrum"
60
+ network: text("network").notNull().default("mainnet"), // "mainnet", "base", "arbitrum"
52
61
  contractAddress: text("contract_address"), // null for native (ETH, BTC)
53
62
  decimals: integer("decimals").notNull(),
54
63
  displayName: text("display_name").notNull(),
@@ -58,8 +67,53 @@ export const paymentMethods = pgTable("payment_methods", {
58
67
  oracleAddress: text("oracle_address"), // Chainlink feed address for price (null = 1:1 stablecoin)
59
68
  xpub: text("xpub"), // HD wallet extended public key for deposit address derivation
60
69
  confirmations: integer("confirmations").notNull().default(1),
70
+ nextIndex: integer("next_index").notNull().default(0), // atomic derivation counter, never reuses
61
71
  createdAt: text("created_at").notNull().default(sql `(now())`),
62
72
  });
73
+ /**
74
+ * BIP-44 path allocation registry — tracks which derivation paths are in use.
75
+ * The server knows which paths are allocated so you never collide.
76
+ * The seed phrase never touches the server — only xpubs.
77
+ */
78
+ export const pathAllocations = pgTable("path_allocations", {
79
+ coinType: integer("coin_type").notNull(), // BIP44 coin type (0=BTC, 60=ETH, 3=DOGE, 501=SOL)
80
+ accountIndex: integer("account_index").notNull(), // m/44'/{coin_type}'/{index}'
81
+ chainId: text("chain_id").references(() => paymentMethods.id),
82
+ xpub: text("xpub").notNull(),
83
+ allocatedAt: text("allocated_at").notNull().default(sql `(now())`),
84
+ }, (table) => [primaryKey({ columns: [table.coinType, table.accountIndex] })]);
85
+ /**
86
+ * Webhook delivery outbox — durable retry for payment callbacks.
87
+ * Inserted when a payment is confirmed. Retried until the receiver ACKs.
88
+ */
89
+ export const webhookDeliveries = pgTable("webhook_deliveries", {
90
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
91
+ chargeId: text("charge_id").notNull(),
92
+ callbackUrl: text("callback_url").notNull(),
93
+ payload: text("payload").notNull(), // JSON stringified
94
+ status: text("status").notNull().default("pending"), // pending, delivered, failed
95
+ attempts: integer("attempts").notNull().default(0),
96
+ nextRetryAt: text("next_retry_at"),
97
+ lastError: text("last_error"),
98
+ createdAt: text("created_at").notNull().default(sql `(now())`),
99
+ }, (table) => [
100
+ index("idx_webhook_deliveries_status").on(table.status),
101
+ index("idx_webhook_deliveries_charge").on(table.chargeId),
102
+ ]);
103
+ /**
104
+ * Every address ever derived — immutable append-only log.
105
+ * Used for auditing and ensuring no address is ever reused.
106
+ */
107
+ export const derivedAddresses = pgTable("derived_addresses", {
108
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
109
+ chainId: text("chain_id")
110
+ .notNull()
111
+ .references(() => paymentMethods.id),
112
+ derivationIndex: integer("derivation_index").notNull(),
113
+ address: text("address").notNull().unique(),
114
+ tenantId: text("tenant_id"),
115
+ createdAt: text("created_at").notNull().default(sql `(now())`),
116
+ }, (table) => [index("idx_derived_addresses_chain").on(table.chainId)]);
63
117
  /** Processed transaction IDs for watchers without block cursors (e.g. BTC). */
64
118
  export const watcherProcessed = pgTable("watcher_processed", {
65
119
  watcherId: text("watcher_id").notNull(),