@wopr-network/platform-core 1.57.0 → 1.58.1

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 (46) hide show
  1. package/dist/billing/crypto/__tests__/address-gen.test.js +118 -0
  2. package/dist/billing/crypto/__tests__/key-server.test.js +3 -0
  3. package/dist/billing/crypto/address-gen.d.ts +24 -0
  4. package/dist/billing/crypto/address-gen.js +137 -0
  5. package/dist/billing/crypto/btc/checkout.js +5 -2
  6. package/dist/billing/crypto/btc/index.d.ts +2 -2
  7. package/dist/billing/crypto/btc/index.js +1 -1
  8. package/dist/billing/crypto/evm/checkout.js +2 -2
  9. package/dist/billing/crypto/evm/eth-checkout.js +2 -2
  10. package/dist/billing/crypto/evm/index.d.ts +2 -1
  11. package/dist/billing/crypto/evm/index.js +1 -1
  12. package/dist/billing/crypto/key-server-entry.js +1 -0
  13. package/dist/billing/crypto/key-server.js +26 -19
  14. package/dist/billing/crypto/payment-method-store.d.ts +1 -0
  15. package/dist/billing/crypto/payment-method-store.js +3 -0
  16. package/dist/billing/crypto/watcher-service.d.ts +2 -0
  17. package/dist/billing/crypto/watcher-service.js +7 -3
  18. package/dist/db/schema/crypto.d.ts +17 -0
  19. package/dist/db/schema/crypto.js +1 -0
  20. package/drizzle/migrations/0020_encoding_params_column.sql +7 -0
  21. package/package.json +1 -1
  22. package/src/billing/crypto/__tests__/address-gen.test.ts +145 -0
  23. package/src/billing/crypto/__tests__/key-server.test.ts +3 -0
  24. package/src/billing/crypto/address-gen.ts +146 -0
  25. package/src/billing/crypto/btc/checkout.ts +5 -2
  26. package/src/billing/crypto/btc/index.ts +2 -2
  27. package/src/billing/crypto/evm/checkout.ts +2 -2
  28. package/src/billing/crypto/evm/eth-checkout.ts +2 -2
  29. package/src/billing/crypto/evm/index.ts +2 -1
  30. package/src/billing/crypto/key-server-entry.ts +1 -0
  31. package/src/billing/crypto/key-server.ts +28 -24
  32. package/src/billing/crypto/payment-method-store.ts +4 -0
  33. package/src/billing/crypto/watcher-service.ts +8 -2
  34. package/src/db/schema/crypto.ts +1 -0
  35. package/dist/billing/crypto/btc/__tests__/address-gen.test.js +0 -44
  36. package/dist/billing/crypto/btc/address-gen.d.ts +0 -23
  37. package/dist/billing/crypto/btc/address-gen.js +0 -101
  38. package/dist/billing/crypto/evm/__tests__/address-gen.test.d.ts +0 -1
  39. package/dist/billing/crypto/evm/__tests__/address-gen.test.js +0 -54
  40. package/dist/billing/crypto/evm/address-gen.d.ts +0 -8
  41. package/dist/billing/crypto/evm/address-gen.js +0 -29
  42. package/src/billing/crypto/btc/__tests__/address-gen.test.ts +0 -53
  43. package/src/billing/crypto/btc/address-gen.ts +0 -122
  44. package/src/billing/crypto/evm/__tests__/address-gen.test.ts +0 -63
  45. package/src/billing/crypto/evm/address-gen.ts +0 -29
  46. /package/dist/billing/crypto/{btc/__tests__ → __tests__}/address-gen.test.d.ts +0 -0
@@ -0,0 +1,118 @@
1
+ import { HDKey } from "@scure/bip32";
2
+ import { describe, expect, it } from "vitest";
3
+ import { deriveAddress, deriveTreasury, isValidXpub } from "../address-gen.js";
4
+ function makeTestXpub(path) {
5
+ const seed = new Uint8Array(32);
6
+ seed[0] = 1;
7
+ const master = HDKey.fromMasterSeed(seed);
8
+ return master.derive(path).publicExtendedKey;
9
+ }
10
+ const BTC_XPUB = makeTestXpub("m/44'/0'/0'");
11
+ const ETH_XPUB = makeTestXpub("m/44'/60'/0'");
12
+ describe("deriveAddress — bech32 (BTC)", () => {
13
+ it("derives a valid bc1q address", () => {
14
+ const addr = deriveAddress(BTC_XPUB, 0, "bech32", { hrp: "bc" });
15
+ expect(addr).toMatch(/^bc1q[a-z0-9]+$/);
16
+ });
17
+ it("derives different addresses for different indices", () => {
18
+ const a = deriveAddress(BTC_XPUB, 0, "bech32", { hrp: "bc" });
19
+ const b = deriveAddress(BTC_XPUB, 1, "bech32", { hrp: "bc" });
20
+ expect(a).not.toBe(b);
21
+ });
22
+ it("is deterministic", () => {
23
+ const a = deriveAddress(BTC_XPUB, 42, "bech32", { hrp: "bc" });
24
+ const b = deriveAddress(BTC_XPUB, 42, "bech32", { hrp: "bc" });
25
+ expect(a).toBe(b);
26
+ });
27
+ it("uses tb prefix for testnet", () => {
28
+ const addr = deriveAddress(BTC_XPUB, 0, "bech32", { hrp: "tb" });
29
+ expect(addr).toMatch(/^tb1q[a-z0-9]+$/);
30
+ });
31
+ it("rejects negative index", () => {
32
+ expect(() => deriveAddress(BTC_XPUB, -1, "bech32", { hrp: "bc" })).toThrow("Invalid");
33
+ });
34
+ it("throws without hrp param", () => {
35
+ expect(() => deriveAddress(BTC_XPUB, 0, "bech32", {})).toThrow("hrp");
36
+ });
37
+ });
38
+ describe("deriveAddress — bech32 (LTC)", () => {
39
+ const LTC_XPUB = makeTestXpub("m/44'/2'/0'");
40
+ it("derives a valid ltc1q address", () => {
41
+ const addr = deriveAddress(LTC_XPUB, 0, "bech32", { hrp: "ltc" });
42
+ expect(addr).toMatch(/^ltc1q[a-z0-9]+$/);
43
+ });
44
+ });
45
+ describe("deriveAddress — p2pkh (DOGE)", () => {
46
+ const DOGE_XPUB = makeTestXpub("m/44'/3'/0'");
47
+ it("derives a valid D... address", () => {
48
+ const addr = deriveAddress(DOGE_XPUB, 0, "p2pkh", { version: "0x1e" });
49
+ expect(addr).toMatch(/^D[a-km-zA-HJ-NP-Z1-9]+$/);
50
+ });
51
+ it("derives different addresses for different indices", () => {
52
+ const a = deriveAddress(DOGE_XPUB, 0, "p2pkh", { version: "0x1e" });
53
+ const b = deriveAddress(DOGE_XPUB, 1, "p2pkh", { version: "0x1e" });
54
+ expect(a).not.toBe(b);
55
+ });
56
+ it("throws without version param", () => {
57
+ expect(() => deriveAddress(DOGE_XPUB, 0, "p2pkh", {})).toThrow("version");
58
+ });
59
+ });
60
+ describe("deriveAddress — p2pkh (TRON)", () => {
61
+ const TRON_XPUB = makeTestXpub("m/44'/195'/0'");
62
+ it("derives a valid T... address", () => {
63
+ const addr = deriveAddress(TRON_XPUB, 0, "p2pkh", { version: "0x41" });
64
+ expect(addr).toMatch(/^T[a-km-zA-HJ-NP-Z1-9]+$/);
65
+ });
66
+ it("is deterministic", () => {
67
+ const a = deriveAddress(TRON_XPUB, 5, "p2pkh", { version: "0x41" });
68
+ const b = deriveAddress(TRON_XPUB, 5, "p2pkh", { version: "0x41" });
69
+ expect(a).toBe(b);
70
+ });
71
+ });
72
+ describe("deriveAddress — evm (ETH)", () => {
73
+ it("derives a valid Ethereum address", () => {
74
+ const addr = deriveAddress(ETH_XPUB, 0, "evm");
75
+ expect(addr).toMatch(/^0x[0-9a-fA-F]{40}$/);
76
+ });
77
+ it("derives different addresses for different indices", () => {
78
+ const a = deriveAddress(ETH_XPUB, 0, "evm");
79
+ const b = deriveAddress(ETH_XPUB, 1, "evm");
80
+ expect(a).not.toBe(b);
81
+ });
82
+ it("is deterministic", () => {
83
+ const a = deriveAddress(ETH_XPUB, 42, "evm");
84
+ const b = deriveAddress(ETH_XPUB, 42, "evm");
85
+ expect(a).toBe(b);
86
+ });
87
+ it("returns checksummed address", () => {
88
+ const addr = deriveAddress(ETH_XPUB, 0, "evm");
89
+ expect(addr).toMatch(/^0x[0-9a-fA-F]{40}$/);
90
+ });
91
+ });
92
+ describe("deriveAddress — unknown type", () => {
93
+ it("throws for unknown address type", () => {
94
+ expect(() => deriveAddress(BTC_XPUB, 0, "foo")).toThrow("Unknown address type");
95
+ });
96
+ });
97
+ describe("deriveTreasury", () => {
98
+ it("derives a valid bech32 treasury address", () => {
99
+ const addr = deriveTreasury(BTC_XPUB, "bech32", { hrp: "bc" });
100
+ expect(addr).toMatch(/^bc1q[a-z0-9]+$/);
101
+ });
102
+ it("differs from deposit address at index 0", () => {
103
+ const deposit = deriveAddress(BTC_XPUB, 0, "bech32", { hrp: "bc" });
104
+ const treasury = deriveTreasury(BTC_XPUB, "bech32", { hrp: "bc" });
105
+ expect(deposit).not.toBe(treasury);
106
+ });
107
+ });
108
+ describe("isValidXpub", () => {
109
+ it("accepts valid xpub", () => {
110
+ expect(isValidXpub(BTC_XPUB)).toBe(true);
111
+ });
112
+ it("rejects garbage", () => {
113
+ expect(isValidXpub("not-an-xpub")).toBe(false);
114
+ });
115
+ it("rejects empty string", () => {
116
+ expect(isValidXpub("")).toBe(false);
117
+ });
118
+ });
@@ -11,6 +11,7 @@ function createMockDb() {
11
11
  nextIndex: 1,
12
12
  decimals: 8,
13
13
  addressType: "bech32",
14
+ encodingParams: '{"hrp":"bc"}',
14
15
  confirmations: 6,
15
16
  };
16
17
  const db = {
@@ -178,6 +179,7 @@ describe("key-server routes", () => {
178
179
  nextIndex: 0,
179
180
  decimals: 18,
180
181
  addressType: "evm",
182
+ encodingParams: "{}",
181
183
  confirmations: 1,
182
184
  };
183
185
  const db = {
@@ -236,6 +238,7 @@ describe("key-server routes", () => {
236
238
  nextIndex: 0,
237
239
  decimals: 18,
238
240
  addressType: "evm",
241
+ encodingParams: "{}",
239
242
  confirmations: 1,
240
243
  };
241
244
  const db = {
@@ -0,0 +1,24 @@
1
+ export interface EncodingParams {
2
+ /** Bech32 human-readable prefix (e.g. "bc", "ltc", "tb"). */
3
+ hrp?: string;
4
+ /** Base58Check version byte as hex string (e.g. "0x1e" for DOGE, "0x41" for TRON). */
5
+ version?: string;
6
+ }
7
+ /**
8
+ * Derive a deposit address from an xpub at a given BIP-44 index.
9
+ * Path: xpub / 0 / index (external chain).
10
+ * No private keys involved.
11
+ *
12
+ * @param xpub - Extended public key
13
+ * @param index - Derivation index (0, 1, 2, ...)
14
+ * @param addressType - Encoding type: "bech32", "p2pkh", "evm"
15
+ * @param params - Chain-specific encoding params from DB (parsed JSON)
16
+ */
17
+ export declare function deriveAddress(xpub: string, index: number, addressType: string, params?: EncodingParams): string;
18
+ /**
19
+ * Derive the treasury address (internal chain, index 0).
20
+ * Used for sweep destinations.
21
+ */
22
+ export declare function deriveTreasury(xpub: string, addressType: string, params?: EncodingParams): string;
23
+ /** Validate that a string is an xpub (not xprv). */
24
+ export declare function isValidXpub(key: string): boolean;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Universal address derivation — one function for all secp256k1 chains.
3
+ *
4
+ * Encoding type and chain-specific params (HRP, version byte) are read from
5
+ * the paymentMethods DB row, not hardcoded per chain. Adding a new chain
6
+ * is a DB INSERT, not a code change.
7
+ *
8
+ * Supported encodings:
9
+ * - bech32: BTC (bc1q...), LTC (ltc1q...) — params: { hrp }
10
+ * - p2pkh: DOGE (D...), TRON (T...) — params: { version }
11
+ * - evm: ETH, ERC-20 (0x...) — params: {}
12
+ */
13
+ import { ripemd160 } from "@noble/hashes/legacy.js";
14
+ import { sha256 } from "@noble/hashes/sha2.js";
15
+ import { bech32 } from "@scure/base";
16
+ import { HDKey } from "@scure/bip32";
17
+ import { publicKeyToAddress } from "viem/accounts";
18
+ // ---------- encoding helpers ----------
19
+ const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
20
+ function base58encode(data) {
21
+ let num = 0n;
22
+ for (const byte of data)
23
+ num = num * 256n + BigInt(byte);
24
+ let encoded = "";
25
+ while (num > 0n) {
26
+ encoded = BASE58_ALPHABET[Number(num % 58n)] + encoded;
27
+ num = num / 58n;
28
+ }
29
+ for (const byte of data) {
30
+ if (byte !== 0)
31
+ break;
32
+ encoded = `1${encoded}`;
33
+ }
34
+ return encoded;
35
+ }
36
+ function hash160(pubkey) {
37
+ return ripemd160(sha256(pubkey));
38
+ }
39
+ function encodeBech32(pubkey, hrp) {
40
+ const h = hash160(pubkey);
41
+ const words = bech32.toWords(h);
42
+ return bech32.encode(hrp, [0, ...words]);
43
+ }
44
+ function encodeP2pkh(pubkey, versionByte) {
45
+ const h = hash160(pubkey);
46
+ const payload = new Uint8Array(21);
47
+ payload[0] = versionByte;
48
+ payload.set(h, 1);
49
+ const checksum = sha256(sha256(payload));
50
+ const full = new Uint8Array(25);
51
+ full.set(payload);
52
+ full.set(checksum.slice(0, 4), 21);
53
+ return base58encode(full);
54
+ }
55
+ function encodeEvm(pubkey) {
56
+ const hexPubKey = `0x${Array.from(pubkey, (b) => b.toString(16).padStart(2, "0")).join("")}`;
57
+ return publicKeyToAddress(hexPubKey);
58
+ }
59
+ // ---------- public API ----------
60
+ /**
61
+ * Derive a deposit address from an xpub at a given BIP-44 index.
62
+ * Path: xpub / 0 / index (external chain).
63
+ * No private keys involved.
64
+ *
65
+ * @param xpub - Extended public key
66
+ * @param index - Derivation index (0, 1, 2, ...)
67
+ * @param addressType - Encoding type: "bech32", "p2pkh", "evm"
68
+ * @param params - Chain-specific encoding params from DB (parsed JSON)
69
+ */
70
+ export function deriveAddress(xpub, index, addressType, params = {}) {
71
+ if (!Number.isInteger(index) || index < 0)
72
+ throw new Error(`Invalid derivation index: ${index}`);
73
+ const master = HDKey.fromExtendedKey(xpub);
74
+ const child = master.deriveChild(0).deriveChild(index);
75
+ if (!child.publicKey)
76
+ throw new Error("Failed to derive public key");
77
+ switch (addressType) {
78
+ case "bech32": {
79
+ if (!params.hrp)
80
+ throw new Error("bech32 encoding requires 'hrp' param");
81
+ return encodeBech32(child.publicKey, params.hrp);
82
+ }
83
+ case "p2pkh": {
84
+ if (!params.version)
85
+ throw new Error("p2pkh encoding requires 'version' param");
86
+ const versionByte = Number(params.version);
87
+ if (!Number.isInteger(versionByte) || versionByte < 0 || versionByte > 255)
88
+ throw new Error(`Invalid p2pkh version byte: ${params.version}`);
89
+ return encodeP2pkh(child.publicKey, versionByte);
90
+ }
91
+ case "evm":
92
+ return encodeEvm(child.publicKey);
93
+ default:
94
+ throw new Error(`Unknown address type: ${addressType}`);
95
+ }
96
+ }
97
+ /**
98
+ * Derive the treasury address (internal chain, index 0).
99
+ * Used for sweep destinations.
100
+ */
101
+ export function deriveTreasury(xpub, addressType, params = {}) {
102
+ const master = HDKey.fromExtendedKey(xpub);
103
+ const child = master.deriveChild(1).deriveChild(0); // internal chain
104
+ if (!child.publicKey)
105
+ throw new Error("Failed to derive public key");
106
+ switch (addressType) {
107
+ case "bech32": {
108
+ if (!params.hrp)
109
+ throw new Error("bech32 encoding requires 'hrp' param");
110
+ return encodeBech32(child.publicKey, params.hrp);
111
+ }
112
+ case "p2pkh": {
113
+ if (!params.version)
114
+ throw new Error("p2pkh encoding requires 'version' param");
115
+ const versionByte = Number(params.version);
116
+ if (!Number.isInteger(versionByte) || versionByte < 0 || versionByte > 255)
117
+ throw new Error(`Invalid p2pkh version byte: ${params.version}`);
118
+ return encodeP2pkh(child.publicKey, versionByte);
119
+ }
120
+ case "evm":
121
+ return encodeEvm(child.publicKey);
122
+ default:
123
+ throw new Error(`Unknown address type: ${addressType}`);
124
+ }
125
+ }
126
+ /** Validate that a string is an xpub (not xprv). */
127
+ export function isValidXpub(key) {
128
+ if (!key.startsWith("xpub"))
129
+ return false;
130
+ try {
131
+ HDKey.fromExtendedKey(key);
132
+ return true;
133
+ }
134
+ catch {
135
+ return false;
136
+ }
137
+ }
@@ -1,5 +1,5 @@
1
1
  import { Credit } from "../../../credits/credit.js";
2
- import { deriveBtcAddress } from "./address-gen.js";
2
+ import { deriveAddress } from "../address-gen.js";
3
3
  export const MIN_BTC_USD = 10;
4
4
  /**
5
5
  * Create a BTC checkout — derive a unique deposit address, store the charge.
@@ -17,7 +17,10 @@ export async function createBtcCheckout(deps, opts) {
17
17
  const maxRetries = 3;
18
18
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
19
19
  const derivationIndex = await deps.chargeStore.getNextDerivationIndex();
20
- const depositAddress = deriveBtcAddress(deps.xpub, derivationIndex, network);
20
+ const hrpMap = { mainnet: "bc", testnet: "tb", regtest: "bcrt" };
21
+ const depositAddress = deriveAddress(deps.xpub, derivationIndex, "bech32", {
22
+ hrp: hrpMap[network],
23
+ });
21
24
  const referenceId = `btc:${depositAddress}`;
22
25
  try {
23
26
  await deps.chargeStore.createStablecoinCharge({
@@ -1,5 +1,5 @@
1
- export type { UtxoChain, UtxoNetwork } from "./address-gen.js";
2
- export { deriveAddress, deriveBtcAddress, deriveBtcTreasury, deriveTreasury } from "./address-gen.js";
1
+ export type { EncodingParams } from "../address-gen.js";
2
+ export { deriveAddress, deriveTreasury, isValidXpub } from "../address-gen.js";
3
3
  export type { BtcCheckoutDeps, BtcCheckoutResult } from "./checkout.js";
4
4
  export { createBtcCheckout, MIN_BTC_USD } from "./checkout.js";
5
5
  export { centsToSats, loadBitcoindConfig, satsToCents } from "./config.js";
@@ -1,4 +1,4 @@
1
- export { deriveAddress, deriveBtcAddress, deriveBtcTreasury, deriveTreasury } from "./address-gen.js";
1
+ export { deriveAddress, deriveTreasury, isValidXpub } from "../address-gen.js";
2
2
  export { createBtcCheckout, MIN_BTC_USD } from "./checkout.js";
3
3
  export { centsToSats, loadBitcoindConfig, satsToCents } from "./config.js";
4
4
  export { settleBtcPayment } from "./settler.js";
@@ -1,5 +1,5 @@
1
1
  import { Credit } from "../../../credits/credit.js";
2
- import { deriveDepositAddress } from "./address-gen.js";
2
+ import { deriveAddress } from "../address-gen.js";
3
3
  import { getTokenConfig, tokenAmountFromCents } from "./config.js";
4
4
  export const MIN_STABLECOIN_USD = 10;
5
5
  /**
@@ -23,7 +23,7 @@ export async function createStablecoinCheckout(deps, opts) {
23
23
  const maxRetries = 3;
24
24
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
25
25
  const derivationIndex = await deps.chargeStore.getNextDerivationIndex();
26
- const depositAddress = deriveDepositAddress(deps.xpub, derivationIndex);
26
+ const depositAddress = deriveAddress(deps.xpub, derivationIndex, "evm");
27
27
  const referenceId = `sc:${opts.chain}:${opts.token.toLowerCase()}:${depositAddress.toLowerCase()}`;
28
28
  try {
29
29
  await deps.chargeStore.createStablecoinCharge({
@@ -1,6 +1,6 @@
1
1
  import { Credit } from "../../../credits/credit.js";
2
+ import { deriveAddress } from "../address-gen.js";
2
3
  import { centsToNative } from "../oracle/convert.js";
3
- import { deriveDepositAddress } from "./address-gen.js";
4
4
  export const MIN_ETH_USD = 10;
5
5
  /**
6
6
  * Create an ETH checkout — derive deposit address, lock price, store charge.
@@ -21,7 +21,7 @@ export async function createEthCheckout(deps, opts) {
21
21
  const maxRetries = 3;
22
22
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
23
23
  const derivationIndex = await deps.chargeStore.getNextDerivationIndex();
24
- const depositAddress = deriveDepositAddress(deps.xpub, derivationIndex);
24
+ const depositAddress = deriveAddress(deps.xpub, derivationIndex, "evm");
25
25
  const referenceId = `eth:${opts.chain}:${depositAddress}`;
26
26
  try {
27
27
  await deps.chargeStore.createStablecoinCharge({
@@ -1,4 +1,5 @@
1
- export { deriveDepositAddress, isValidXpub } from "./address-gen.js";
1
+ export type { EncodingParams } from "../address-gen.js";
2
+ export { deriveAddress, isValidXpub } from "../address-gen.js";
2
3
  export type { StablecoinCheckoutDeps, StablecoinCheckoutResult } from "./checkout.js";
3
4
  export { createStablecoinCheckout, MIN_STABLECOIN_USD } from "./checkout.js";
4
5
  export { centsFromTokenAmount, getChainConfig, getTokenConfig, tokenAmountFromCents } from "./config.js";
@@ -1,4 +1,4 @@
1
- export { deriveDepositAddress, isValidXpub } from "./address-gen.js";
1
+ export { deriveAddress, isValidXpub } from "../address-gen.js";
2
2
  export { createStablecoinCheckout, MIN_STABLECOIN_USD } from "./checkout.js";
3
3
  export { centsFromTokenAmount, getChainConfig, getTokenConfig, tokenAmountFromCents } from "./config.js";
4
4
  export { createEthCheckout, MIN_ETH_USD } from "./eth-checkout.js";
@@ -68,6 +68,7 @@ async function main() {
68
68
  oracle,
69
69
  bitcoindUser: BITCOIND_USER,
70
70
  bitcoindPassword: BITCOIND_PASSWORD,
71
+ serviceKey: SERVICE_KEY,
71
72
  log: (msg, meta) => console.log(`[watcher] ${msg}`, meta ?? ""),
72
73
  });
73
74
  const server = serve({ fetch: app.fetch, port: PORT });
@@ -10,8 +10,7 @@
10
10
  import { eq, sql } from "drizzle-orm";
11
11
  import { Hono } from "hono";
12
12
  import { derivedAddresses, pathAllocations, paymentMethods } from "../../db/schema/crypto.js";
13
- import { deriveAddress, deriveP2pkhAddress } from "./btc/address-gen.js";
14
- import { deriveDepositAddress } from "./evm/address-gen.js";
13
+ import { deriveAddress } from "./address-gen.js";
15
14
  import { centsToNative } from "./oracle/convert.js";
16
15
  import { AssetNotSupportedError } from "./oracle/types.js";
17
16
  /**
@@ -38,21 +37,16 @@ async function deriveNextAddress(db, chainId, tenantId) {
38
37
  if (!method.xpub)
39
38
  throw new Error(`No xpub configured for chain: ${chainId}`);
40
39
  const index = method.nextIndex - 1;
41
- // Route to the right derivation function via address_type (DB-driven, no hardcoded chains)
42
- let address;
43
- switch (method.addressType) {
44
- case "bech32":
45
- address = deriveAddress(method.xpub, index, (method.network ?? "mainnet"), method.chain);
46
- break;
47
- case "p2pkh":
48
- address = deriveP2pkhAddress(method.xpub, index, method.chain);
49
- break;
50
- case "evm":
51
- address = deriveDepositAddress(method.xpub, index);
52
- break;
53
- default:
54
- throw new Error(`Unknown address type: ${method.addressType}`);
40
+ // Universal address derivation encoding type + params are DB-driven.
41
+ // Adding a new chain is a DB INSERT, not a code change.
42
+ let encodingParams = {};
43
+ try {
44
+ encodingParams = JSON.parse(method.encodingParams ?? "{}");
45
+ }
46
+ catch {
47
+ throw new Error(`Invalid encoding_params JSON for chain ${chainId}: ${method.encodingParams}`);
55
48
  }
49
+ const address = deriveAddress(method.xpub, index, method.addressType, encodingParams);
56
50
  // Step 2: Record in immutable log. If this address was already derived by a
57
51
  // sibling chain (shared xpub), the unique constraint fires and we retry
58
52
  // with the next index (which is already incremented above).
@@ -177,9 +171,12 @@ export function createKeyServerApp(deps) {
177
171
  callbackUrl: body.callbackUrl,
178
172
  expectedAmount: expectedAmount.toString(),
179
173
  });
180
- // Format display amount for the client
181
- const divisor = 10 ** method.decimals;
182
- const displayAmount = `${(Number(expectedAmount) / divisor).toFixed(Math.min(method.decimals, 8))} ${token}`;
174
+ // Format display amount for the client (BigInt-safe, no Number overflow)
175
+ const divisor = 10n ** BigInt(method.decimals);
176
+ const whole = expectedAmount / divisor;
177
+ const frac = expectedAmount % divisor;
178
+ const fracStr = frac.toString().padStart(method.decimals, "0").slice(0, 8).replace(/0+$/, "");
179
+ const displayAmount = `${whole}${fracStr ? `.${fracStr}` : ""} ${token}`;
183
180
  return c.json({
184
181
  chargeId: referenceId,
185
182
  address,
@@ -273,6 +270,15 @@ export function createKeyServerApp(deps) {
273
270
  if (inserted.rowCount === 0) {
274
271
  return c.json({ error: "Path allocation already exists", path: `m/44'/${body.coin_type}'/${body.account_index}'` }, 409);
275
272
  }
273
+ // Validate encoding_params match address_type requirements
274
+ const addrType = body.address_type ?? "evm";
275
+ const encParams = body.encoding_params ?? {};
276
+ if (addrType === "bech32" && !encParams.hrp) {
277
+ return c.json({ error: "bech32 address_type requires encoding_params.hrp" }, 400);
278
+ }
279
+ if (addrType === "p2pkh" && !encParams.version) {
280
+ return c.json({ error: "p2pkh address_type requires encoding_params.version" }, 400);
281
+ }
276
282
  // Upsert the payment method
277
283
  await deps.methodStore.upsert({
278
284
  id: body.id,
@@ -289,6 +295,7 @@ export function createKeyServerApp(deps) {
289
295
  oracleAddress: body.oracle_address ?? null,
290
296
  xpub: body.xpub,
291
297
  addressType: body.address_type ?? "evm",
298
+ encodingParams: JSON.stringify(body.encoding_params ?? {}),
292
299
  confirmations: body.confirmations ?? 6,
293
300
  });
294
301
  return c.json({ id: body.id, path: `m/44'/${body.coin_type}'/${body.account_index}'` }, 201);
@@ -14,6 +14,7 @@ export interface PaymentMethodRecord {
14
14
  oracleAddress: string | null;
15
15
  xpub: string | null;
16
16
  addressType: string;
17
+ encodingParams: string;
17
18
  confirmations: number;
18
19
  }
19
20
  export interface IPaymentMethodStore {
@@ -47,6 +47,7 @@ export class DrizzlePaymentMethodStore {
47
47
  oracleAddress: method.oracleAddress,
48
48
  xpub: method.xpub,
49
49
  addressType: method.addressType,
50
+ encodingParams: method.encodingParams,
50
51
  confirmations: method.confirmations,
51
52
  })
52
53
  .onConflictDoUpdate({
@@ -65,6 +66,7 @@ export class DrizzlePaymentMethodStore {
65
66
  oracleAddress: method.oracleAddress,
66
67
  xpub: method.xpub,
67
68
  addressType: method.addressType,
69
+ encodingParams: method.encodingParams,
68
70
  confirmations: method.confirmations,
69
71
  },
70
72
  });
@@ -102,6 +104,7 @@ function toRecord(row) {
102
104
  oracleAddress: row.oracleAddress,
103
105
  xpub: row.xpub,
104
106
  addressType: row.addressType,
107
+ encodingParams: row.encodingParams,
105
108
  confirmations: row.confirmations,
106
109
  };
107
110
  }
@@ -29,6 +29,8 @@ export interface WatcherServiceOpts {
29
29
  log?: (msg: string, meta?: Record<string, unknown>) => void;
30
30
  /** Allowed callback URL prefixes. Default: ["https://"] — enforces HTTPS. */
31
31
  allowedCallbackPrefixes?: string[];
32
+ /** Service key sent as Bearer token in webhook deliveries. */
33
+ serviceKey?: string;
32
34
  }
33
35
  export interface PaymentPayload {
34
36
  txHash: string;
@@ -43,7 +43,7 @@ async function enqueueWebhook(db, chargeId, callbackUrl, payload) {
43
43
  payload: JSON.stringify(payload),
44
44
  });
45
45
  }
46
- async function processDeliveries(db, allowedPrefixes, log) {
46
+ async function processDeliveries(db, allowedPrefixes, log, serviceKey) {
47
47
  const now = new Date().toISOString();
48
48
  const pending = await db
49
49
  .select()
@@ -60,9 +60,12 @@ async function processDeliveries(db, allowedPrefixes, log) {
60
60
  continue;
61
61
  }
62
62
  try {
63
+ const headers = { "Content-Type": "application/json" };
64
+ if (serviceKey)
65
+ headers.Authorization = `Bearer ${serviceKey}`;
63
66
  const res = await fetch(row.callbackUrl, {
64
67
  method: "POST",
65
- headers: { "Content-Type": "application/json" },
68
+ headers,
66
69
  body: row.payload,
67
70
  });
68
71
  if (!res.ok)
@@ -175,6 +178,7 @@ export async function startWatchers(opts) {
175
178
  const deliveryMs = opts.deliveryIntervalMs ?? 10_000;
176
179
  const log = opts.log ?? (() => { });
177
180
  const allowedPrefixes = opts.allowedCallbackPrefixes ?? ["https://"];
181
+ const serviceKey = opts.serviceKey;
178
182
  const timers = [];
179
183
  const methods = await methodStore.listEnabled();
180
184
  const utxoMethods = methods.filter((m) => m.type === "native" && (m.chain === "bitcoin" || m.chain === "litecoin" || m.chain === "dogecoin"));
@@ -388,7 +392,7 @@ export async function startWatchers(opts) {
388
392
  // --- Webhook delivery outbox processor ---
389
393
  timers.push(setInterval(async () => {
390
394
  try {
391
- const count = await processDeliveries(db, allowedPrefixes, log);
395
+ const count = await processDeliveries(db, allowedPrefixes, log, serviceKey);
392
396
  if (count > 0)
393
397
  log("Webhooks delivered", { count });
394
398
  }
@@ -682,6 +682,23 @@ export declare const paymentMethods: import("drizzle-orm/pg-core").PgTableWithCo
682
682
  identity: undefined;
683
683
  generated: undefined;
684
684
  }, {}, {}>;
685
+ encodingParams: import("drizzle-orm/pg-core").PgColumn<{
686
+ name: "encoding_params";
687
+ tableName: "payment_methods";
688
+ dataType: "string";
689
+ columnType: "PgText";
690
+ data: string;
691
+ driverParam: string;
692
+ notNull: true;
693
+ hasDefault: true;
694
+ isPrimaryKey: false;
695
+ isAutoincrement: false;
696
+ hasRuntimeDefault: false;
697
+ enumValues: [string, ...string[]];
698
+ baseColumn: never;
699
+ identity: undefined;
700
+ generated: undefined;
701
+ }, {}, {}>;
685
702
  confirmations: import("drizzle-orm/pg-core").PgColumn<{
686
703
  name: "confirmations";
687
704
  tableName: "payment_methods";
@@ -76,6 +76,7 @@ export const paymentMethods = pgTable("payment_methods", {
76
76
  oracleAddress: text("oracle_address"), // Chainlink feed address for price (null = 1:1 stablecoin)
77
77
  xpub: text("xpub"), // HD wallet extended public key for deposit address derivation
78
78
  addressType: text("address_type").notNull().default("evm"), // "bech32" (BTC/LTC), "p2pkh" (DOGE), "evm" (ETH/ERC20)
79
+ encodingParams: text("encoding_params").notNull().default("{}"), // JSON: {"hrp":"bc"}, {"version":"0x1e"}, etc.
79
80
  confirmations: integer("confirmations").notNull().default(1),
80
81
  nextIndex: integer("next_index").notNull().default(0), // atomic derivation counter, never reuses
81
82
  createdAt: text("created_at").notNull().default(sql `(now())`),
@@ -0,0 +1,7 @@
1
+ ALTER TABLE "payment_methods" ADD COLUMN "encoding_params" text DEFAULT '{}' NOT NULL;
2
+ --> statement-breakpoint
3
+ UPDATE "payment_methods" SET "encoding_params" = '{"hrp":"bc"}' WHERE "address_type" = 'bech32' AND "chain" = 'bitcoin';
4
+ --> statement-breakpoint
5
+ UPDATE "payment_methods" SET "encoding_params" = '{"hrp":"ltc"}' WHERE "address_type" = 'bech32' AND "chain" = 'litecoin';
6
+ --> statement-breakpoint
7
+ UPDATE "payment_methods" SET "encoding_params" = '{"version":"0x1e"}' WHERE "address_type" = 'p2pkh' AND "chain" = 'dogecoin';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.57.0",
3
+ "version": "1.58.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",