@wopr-network/platform-core 1.57.0 → 1.58.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__/address-gen.test.js +118 -0
- package/dist/billing/crypto/__tests__/key-server.test.js +3 -0
- package/dist/billing/crypto/address-gen.d.ts +24 -0
- package/dist/billing/crypto/address-gen.js +137 -0
- package/dist/billing/crypto/btc/checkout.js +5 -2
- package/dist/billing/crypto/btc/index.d.ts +2 -2
- package/dist/billing/crypto/btc/index.js +1 -1
- package/dist/billing/crypto/evm/checkout.js +2 -2
- package/dist/billing/crypto/evm/eth-checkout.js +2 -2
- package/dist/billing/crypto/evm/index.d.ts +2 -1
- package/dist/billing/crypto/evm/index.js +1 -1
- package/dist/billing/crypto/key-server.js +26 -19
- package/dist/billing/crypto/payment-method-store.d.ts +1 -0
- package/dist/billing/crypto/payment-method-store.js +3 -0
- package/dist/db/schema/crypto.d.ts +17 -0
- package/dist/db/schema/crypto.js +1 -0
- package/drizzle/migrations/0020_encoding_params_column.sql +7 -0
- package/package.json +1 -1
- package/src/billing/crypto/__tests__/address-gen.test.ts +145 -0
- package/src/billing/crypto/__tests__/key-server.test.ts +3 -0
- package/src/billing/crypto/address-gen.ts +146 -0
- package/src/billing/crypto/btc/checkout.ts +5 -2
- package/src/billing/crypto/btc/index.ts +2 -2
- package/src/billing/crypto/evm/checkout.ts +2 -2
- package/src/billing/crypto/evm/eth-checkout.ts +2 -2
- package/src/billing/crypto/evm/index.ts +2 -1
- package/src/billing/crypto/key-server.ts +28 -24
- package/src/billing/crypto/payment-method-store.ts +4 -0
- package/src/db/schema/crypto.ts +1 -0
- package/dist/billing/crypto/btc/__tests__/address-gen.test.js +0 -44
- package/dist/billing/crypto/btc/address-gen.d.ts +0 -23
- package/dist/billing/crypto/btc/address-gen.js +0 -101
- package/dist/billing/crypto/evm/__tests__/address-gen.test.d.ts +0 -1
- package/dist/billing/crypto/evm/__tests__/address-gen.test.js +0 -54
- package/dist/billing/crypto/evm/address-gen.d.ts +0 -8
- package/dist/billing/crypto/evm/address-gen.js +0 -29
- package/src/billing/crypto/btc/__tests__/address-gen.test.ts +0 -53
- package/src/billing/crypto/btc/address-gen.ts +0 -122
- package/src/billing/crypto/evm/__tests__/address-gen.test.ts +0 -63
- package/src/billing/crypto/evm/address-gen.ts +0 -29
- /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 {
|
|
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
|
|
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 {
|
|
2
|
-
export { deriveAddress,
|
|
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,
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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 {
|
|
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";
|
|
@@ -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
|
|
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
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 =
|
|
182
|
-
const
|
|
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);
|
|
@@ -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
|
}
|
|
@@ -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";
|
package/dist/db/schema/crypto.js
CHANGED
|
@@ -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';
|