@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,145 @@
|
|
|
1
|
+
import { HDKey } from "@scure/bip32";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { deriveAddress, deriveTreasury, isValidXpub } from "../address-gen.js";
|
|
4
|
+
|
|
5
|
+
function makeTestXpub(path: string): string {
|
|
6
|
+
const seed = new Uint8Array(32);
|
|
7
|
+
seed[0] = 1;
|
|
8
|
+
const master = HDKey.fromMasterSeed(seed);
|
|
9
|
+
return master.derive(path).publicExtendedKey;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const BTC_XPUB = makeTestXpub("m/44'/0'/0'");
|
|
13
|
+
const ETH_XPUB = makeTestXpub("m/44'/60'/0'");
|
|
14
|
+
|
|
15
|
+
describe("deriveAddress — bech32 (BTC)", () => {
|
|
16
|
+
it("derives a valid bc1q address", () => {
|
|
17
|
+
const addr = deriveAddress(BTC_XPUB, 0, "bech32", { hrp: "bc" });
|
|
18
|
+
expect(addr).toMatch(/^bc1q[a-z0-9]+$/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("derives different addresses for different indices", () => {
|
|
22
|
+
const a = deriveAddress(BTC_XPUB, 0, "bech32", { hrp: "bc" });
|
|
23
|
+
const b = deriveAddress(BTC_XPUB, 1, "bech32", { hrp: "bc" });
|
|
24
|
+
expect(a).not.toBe(b);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("is deterministic", () => {
|
|
28
|
+
const a = deriveAddress(BTC_XPUB, 42, "bech32", { hrp: "bc" });
|
|
29
|
+
const b = deriveAddress(BTC_XPUB, 42, "bech32", { hrp: "bc" });
|
|
30
|
+
expect(a).toBe(b);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("uses tb prefix for testnet", () => {
|
|
34
|
+
const addr = deriveAddress(BTC_XPUB, 0, "bech32", { hrp: "tb" });
|
|
35
|
+
expect(addr).toMatch(/^tb1q[a-z0-9]+$/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("rejects negative index", () => {
|
|
39
|
+
expect(() => deriveAddress(BTC_XPUB, -1, "bech32", { hrp: "bc" })).toThrow("Invalid");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("throws without hrp param", () => {
|
|
43
|
+
expect(() => deriveAddress(BTC_XPUB, 0, "bech32", {})).toThrow("hrp");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("deriveAddress — bech32 (LTC)", () => {
|
|
48
|
+
const LTC_XPUB = makeTestXpub("m/44'/2'/0'");
|
|
49
|
+
|
|
50
|
+
it("derives a valid ltc1q address", () => {
|
|
51
|
+
const addr = deriveAddress(LTC_XPUB, 0, "bech32", { hrp: "ltc" });
|
|
52
|
+
expect(addr).toMatch(/^ltc1q[a-z0-9]+$/);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("deriveAddress — p2pkh (DOGE)", () => {
|
|
57
|
+
const DOGE_XPUB = makeTestXpub("m/44'/3'/0'");
|
|
58
|
+
|
|
59
|
+
it("derives a valid D... address", () => {
|
|
60
|
+
const addr = deriveAddress(DOGE_XPUB, 0, "p2pkh", { version: "0x1e" });
|
|
61
|
+
expect(addr).toMatch(/^D[a-km-zA-HJ-NP-Z1-9]+$/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("derives different addresses for different indices", () => {
|
|
65
|
+
const a = deriveAddress(DOGE_XPUB, 0, "p2pkh", { version: "0x1e" });
|
|
66
|
+
const b = deriveAddress(DOGE_XPUB, 1, "p2pkh", { version: "0x1e" });
|
|
67
|
+
expect(a).not.toBe(b);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("throws without version param", () => {
|
|
71
|
+
expect(() => deriveAddress(DOGE_XPUB, 0, "p2pkh", {})).toThrow("version");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("deriveAddress — p2pkh (TRON)", () => {
|
|
76
|
+
const TRON_XPUB = makeTestXpub("m/44'/195'/0'");
|
|
77
|
+
|
|
78
|
+
it("derives a valid T... address", () => {
|
|
79
|
+
const addr = deriveAddress(TRON_XPUB, 0, "p2pkh", { version: "0x41" });
|
|
80
|
+
expect(addr).toMatch(/^T[a-km-zA-HJ-NP-Z1-9]+$/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("is deterministic", () => {
|
|
84
|
+
const a = deriveAddress(TRON_XPUB, 5, "p2pkh", { version: "0x41" });
|
|
85
|
+
const b = deriveAddress(TRON_XPUB, 5, "p2pkh", { version: "0x41" });
|
|
86
|
+
expect(a).toBe(b);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("deriveAddress — evm (ETH)", () => {
|
|
91
|
+
it("derives a valid Ethereum address", () => {
|
|
92
|
+
const addr = deriveAddress(ETH_XPUB, 0, "evm");
|
|
93
|
+
expect(addr).toMatch(/^0x[0-9a-fA-F]{40}$/);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("derives different addresses for different indices", () => {
|
|
97
|
+
const a = deriveAddress(ETH_XPUB, 0, "evm");
|
|
98
|
+
const b = deriveAddress(ETH_XPUB, 1, "evm");
|
|
99
|
+
expect(a).not.toBe(b);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("is deterministic", () => {
|
|
103
|
+
const a = deriveAddress(ETH_XPUB, 42, "evm");
|
|
104
|
+
const b = deriveAddress(ETH_XPUB, 42, "evm");
|
|
105
|
+
expect(a).toBe(b);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("returns checksummed address", () => {
|
|
109
|
+
const addr = deriveAddress(ETH_XPUB, 0, "evm");
|
|
110
|
+
expect(addr).toMatch(/^0x[0-9a-fA-F]{40}$/);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("deriveAddress — unknown type", () => {
|
|
115
|
+
it("throws for unknown address type", () => {
|
|
116
|
+
expect(() => deriveAddress(BTC_XPUB, 0, "foo")).toThrow("Unknown address type");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("deriveTreasury", () => {
|
|
121
|
+
it("derives a valid bech32 treasury address", () => {
|
|
122
|
+
const addr = deriveTreasury(BTC_XPUB, "bech32", { hrp: "bc" });
|
|
123
|
+
expect(addr).toMatch(/^bc1q[a-z0-9]+$/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("differs from deposit address at index 0", () => {
|
|
127
|
+
const deposit = deriveAddress(BTC_XPUB, 0, "bech32", { hrp: "bc" });
|
|
128
|
+
const treasury = deriveTreasury(BTC_XPUB, "bech32", { hrp: "bc" });
|
|
129
|
+
expect(deposit).not.toBe(treasury);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("isValidXpub", () => {
|
|
134
|
+
it("accepts valid xpub", () => {
|
|
135
|
+
expect(isValidXpub(BTC_XPUB)).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("rejects garbage", () => {
|
|
139
|
+
expect(isValidXpub("not-an-xpub")).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("rejects empty string", () => {
|
|
143
|
+
expect(isValidXpub("")).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -15,6 +15,7 @@ function createMockDb() {
|
|
|
15
15
|
nextIndex: 1,
|
|
16
16
|
decimals: 8,
|
|
17
17
|
addressType: "bech32",
|
|
18
|
+
encodingParams: '{"hrp":"bc"}',
|
|
18
19
|
confirmations: 6,
|
|
19
20
|
};
|
|
20
21
|
|
|
@@ -195,6 +196,7 @@ describe("key-server routes", () => {
|
|
|
195
196
|
nextIndex: 0,
|
|
196
197
|
decimals: 18,
|
|
197
198
|
addressType: "evm",
|
|
199
|
+
encodingParams: "{}",
|
|
198
200
|
confirmations: 1,
|
|
199
201
|
};
|
|
200
202
|
|
|
@@ -258,6 +260,7 @@ describe("key-server routes", () => {
|
|
|
258
260
|
nextIndex: 0,
|
|
259
261
|
decimals: 18,
|
|
260
262
|
addressType: "evm",
|
|
263
|
+
encodingParams: "{}",
|
|
261
264
|
confirmations: 1,
|
|
262
265
|
};
|
|
263
266
|
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
|
|
19
|
+
export interface EncodingParams {
|
|
20
|
+
/** Bech32 human-readable prefix (e.g. "bc", "ltc", "tb"). */
|
|
21
|
+
hrp?: string;
|
|
22
|
+
/** Base58Check version byte as hex string (e.g. "0x1e" for DOGE, "0x41" for TRON). */
|
|
23
|
+
version?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---------- encoding helpers ----------
|
|
27
|
+
|
|
28
|
+
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
29
|
+
function base58encode(data: Uint8Array): string {
|
|
30
|
+
let num = 0n;
|
|
31
|
+
for (const byte of data) num = num * 256n + BigInt(byte);
|
|
32
|
+
let encoded = "";
|
|
33
|
+
while (num > 0n) {
|
|
34
|
+
encoded = BASE58_ALPHABET[Number(num % 58n)] + encoded;
|
|
35
|
+
num = num / 58n;
|
|
36
|
+
}
|
|
37
|
+
for (const byte of data) {
|
|
38
|
+
if (byte !== 0) break;
|
|
39
|
+
encoded = `1${encoded}`;
|
|
40
|
+
}
|
|
41
|
+
return encoded;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hash160(pubkey: Uint8Array): Uint8Array {
|
|
45
|
+
return ripemd160(sha256(pubkey));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function encodeBech32(pubkey: Uint8Array, hrp: string): string {
|
|
49
|
+
const h = hash160(pubkey);
|
|
50
|
+
const words = bech32.toWords(h);
|
|
51
|
+
return bech32.encode(hrp, [0, ...words]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function encodeP2pkh(pubkey: Uint8Array, versionByte: number): string {
|
|
55
|
+
const h = hash160(pubkey);
|
|
56
|
+
const payload = new Uint8Array(21);
|
|
57
|
+
payload[0] = versionByte;
|
|
58
|
+
payload.set(h, 1);
|
|
59
|
+
const checksum = sha256(sha256(payload));
|
|
60
|
+
const full = new Uint8Array(25);
|
|
61
|
+
full.set(payload);
|
|
62
|
+
full.set(checksum.slice(0, 4), 21);
|
|
63
|
+
return base58encode(full);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function encodeEvm(pubkey: Uint8Array): string {
|
|
67
|
+
const hexPubKey = `0x${Array.from(pubkey, (b) => b.toString(16).padStart(2, "0")).join("")}` as `0x${string}`;
|
|
68
|
+
return publicKeyToAddress(hexPubKey);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------- public API ----------
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Derive a deposit address from an xpub at a given BIP-44 index.
|
|
75
|
+
* Path: xpub / 0 / index (external chain).
|
|
76
|
+
* No private keys involved.
|
|
77
|
+
*
|
|
78
|
+
* @param xpub - Extended public key
|
|
79
|
+
* @param index - Derivation index (0, 1, 2, ...)
|
|
80
|
+
* @param addressType - Encoding type: "bech32", "p2pkh", "evm"
|
|
81
|
+
* @param params - Chain-specific encoding params from DB (parsed JSON)
|
|
82
|
+
*/
|
|
83
|
+
export function deriveAddress(xpub: string, index: number, addressType: string, params: EncodingParams = {}): string {
|
|
84
|
+
if (!Number.isInteger(index) || index < 0) throw new Error(`Invalid derivation index: ${index}`);
|
|
85
|
+
|
|
86
|
+
const master = HDKey.fromExtendedKey(xpub);
|
|
87
|
+
const child = master.deriveChild(0).deriveChild(index);
|
|
88
|
+
if (!child.publicKey) throw new Error("Failed to derive public key");
|
|
89
|
+
|
|
90
|
+
switch (addressType) {
|
|
91
|
+
case "bech32": {
|
|
92
|
+
if (!params.hrp) throw new Error("bech32 encoding requires 'hrp' param");
|
|
93
|
+
return encodeBech32(child.publicKey, params.hrp);
|
|
94
|
+
}
|
|
95
|
+
case "p2pkh": {
|
|
96
|
+
if (!params.version) throw new Error("p2pkh encoding requires 'version' param");
|
|
97
|
+
const versionByte = Number(params.version);
|
|
98
|
+
if (!Number.isInteger(versionByte) || versionByte < 0 || versionByte > 255)
|
|
99
|
+
throw new Error(`Invalid p2pkh version byte: ${params.version}`);
|
|
100
|
+
return encodeP2pkh(child.publicKey, versionByte);
|
|
101
|
+
}
|
|
102
|
+
case "evm":
|
|
103
|
+
return encodeEvm(child.publicKey);
|
|
104
|
+
default:
|
|
105
|
+
throw new Error(`Unknown address type: ${addressType}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Derive the treasury address (internal chain, index 0).
|
|
111
|
+
* Used for sweep destinations.
|
|
112
|
+
*/
|
|
113
|
+
export function deriveTreasury(xpub: string, addressType: string, params: EncodingParams = {}): string {
|
|
114
|
+
const master = HDKey.fromExtendedKey(xpub);
|
|
115
|
+
const child = master.deriveChild(1).deriveChild(0); // internal chain
|
|
116
|
+
if (!child.publicKey) throw new Error("Failed to derive public key");
|
|
117
|
+
|
|
118
|
+
switch (addressType) {
|
|
119
|
+
case "bech32": {
|
|
120
|
+
if (!params.hrp) throw new Error("bech32 encoding requires 'hrp' param");
|
|
121
|
+
return encodeBech32(child.publicKey, params.hrp);
|
|
122
|
+
}
|
|
123
|
+
case "p2pkh": {
|
|
124
|
+
if (!params.version) throw new Error("p2pkh encoding requires 'version' param");
|
|
125
|
+
const versionByte = Number(params.version);
|
|
126
|
+
if (!Number.isInteger(versionByte) || versionByte < 0 || versionByte > 255)
|
|
127
|
+
throw new Error(`Invalid p2pkh version byte: ${params.version}`);
|
|
128
|
+
return encodeP2pkh(child.publicKey, versionByte);
|
|
129
|
+
}
|
|
130
|
+
case "evm":
|
|
131
|
+
return encodeEvm(child.publicKey);
|
|
132
|
+
default:
|
|
133
|
+
throw new Error(`Unknown address type: ${addressType}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Validate that a string is an xpub (not xprv). */
|
|
138
|
+
export function isValidXpub(key: string): boolean {
|
|
139
|
+
if (!key.startsWith("xpub")) return false;
|
|
140
|
+
try {
|
|
141
|
+
HDKey.fromExtendedKey(key);
|
|
142
|
+
return true;
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Credit } from "../../../credits/credit.js";
|
|
2
|
+
import { deriveAddress } from "../address-gen.js";
|
|
2
3
|
import type { ICryptoChargeRepository } from "../charge-store.js";
|
|
3
|
-
import { deriveBtcAddress } from "./address-gen.js";
|
|
4
4
|
import type { BtcCheckoutOpts } from "./types.js";
|
|
5
5
|
|
|
6
6
|
export const MIN_BTC_USD = 10;
|
|
@@ -35,7 +35,10 @@ export async function createBtcCheckout(deps: BtcCheckoutDeps, opts: BtcCheckout
|
|
|
35
35
|
|
|
36
36
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
37
37
|
const derivationIndex = await deps.chargeStore.getNextDerivationIndex();
|
|
38
|
-
const
|
|
38
|
+
const hrpMap = { mainnet: "bc", testnet: "tb", regtest: "bcrt" } as const;
|
|
39
|
+
const depositAddress = deriveAddress(deps.xpub, derivationIndex, "bech32", {
|
|
40
|
+
hrp: hrpMap[network],
|
|
41
|
+
});
|
|
39
42
|
const referenceId = `btc:${depositAddress}`;
|
|
40
43
|
|
|
41
44
|
try {
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
import { Credit } from "../../../credits/credit.js";
|
|
2
|
+
import { deriveAddress } from "../address-gen.js";
|
|
2
3
|
import type { ICryptoChargeRepository } from "../charge-store.js";
|
|
3
|
-
import { deriveDepositAddress } from "./address-gen.js";
|
|
4
4
|
import { getTokenConfig, tokenAmountFromCents } from "./config.js";
|
|
5
5
|
import type { StablecoinCheckoutOpts } from "./types.js";
|
|
6
6
|
|
|
@@ -47,7 +47,7 @@ export async function createStablecoinCheckout(
|
|
|
47
47
|
const maxRetries = 3;
|
|
48
48
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
49
49
|
const derivationIndex = await deps.chargeStore.getNextDerivationIndex();
|
|
50
|
-
const depositAddress =
|
|
50
|
+
const depositAddress = deriveAddress(deps.xpub, derivationIndex, "evm");
|
|
51
51
|
const referenceId = `sc:${opts.chain}:${opts.token.toLowerCase()}:${depositAddress.toLowerCase()}`;
|
|
52
52
|
|
|
53
53
|
try {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Credit } from "../../../credits/credit.js";
|
|
2
|
+
import { deriveAddress } from "../address-gen.js";
|
|
2
3
|
import type { ICryptoChargeRepository } from "../charge-store.js";
|
|
3
4
|
import { centsToNative } from "../oracle/convert.js";
|
|
4
5
|
import type { IPriceOracle } from "../oracle/types.js";
|
|
5
|
-
import { deriveDepositAddress } from "./address-gen.js";
|
|
6
6
|
import type { EvmChain } from "./types.js";
|
|
7
7
|
|
|
8
8
|
export const MIN_ETH_USD = 10;
|
|
@@ -51,7 +51,7 @@ export async function createEthCheckout(deps: EthCheckoutDeps, opts: EthCheckout
|
|
|
51
51
|
|
|
52
52
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
53
53
|
const derivationIndex = await deps.chargeStore.getNextDerivationIndex();
|
|
54
|
-
const depositAddress =
|
|
54
|
+
const depositAddress = deriveAddress(deps.xpub, derivationIndex, "evm") as `0x${string}`;
|
|
55
55
|
const referenceId = `eth:${opts.chain}:${depositAddress}`;
|
|
56
56
|
|
|
57
57
|
try {
|
|
@@ -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";
|
|
@@ -11,9 +11,9 @@ import { eq, sql } from "drizzle-orm";
|
|
|
11
11
|
import { Hono } from "hono";
|
|
12
12
|
import type { DrizzleDb } from "../../db/index.js";
|
|
13
13
|
import { derivedAddresses, pathAllocations, paymentMethods } from "../../db/schema/crypto.js";
|
|
14
|
-
import {
|
|
14
|
+
import type { EncodingParams } from "./address-gen.js";
|
|
15
|
+
import { deriveAddress } from "./address-gen.js";
|
|
15
16
|
import type { ICryptoChargeRepository } from "./charge-store.js";
|
|
16
|
-
import { deriveDepositAddress } from "./evm/address-gen.js";
|
|
17
17
|
import { centsToNative } from "./oracle/convert.js";
|
|
18
18
|
import type { IPriceOracle } from "./oracle/types.js";
|
|
19
19
|
import { AssetNotSupportedError } from "./oracle/types.js";
|
|
@@ -60,26 +60,15 @@ async function deriveNextAddress(
|
|
|
60
60
|
|
|
61
61
|
const index = method.nextIndex - 1;
|
|
62
62
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
(method.network ?? "mainnet") as "mainnet" | "testnet" | "regtest",
|
|
71
|
-
method.chain as "bitcoin" | "litecoin",
|
|
72
|
-
);
|
|
73
|
-
break;
|
|
74
|
-
case "p2pkh":
|
|
75
|
-
address = deriveP2pkhAddress(method.xpub, index, method.chain);
|
|
76
|
-
break;
|
|
77
|
-
case "evm":
|
|
78
|
-
address = deriveDepositAddress(method.xpub, index);
|
|
79
|
-
break;
|
|
80
|
-
default:
|
|
81
|
-
throw new Error(`Unknown address type: ${method.addressType}`);
|
|
63
|
+
// Universal address derivation — encoding type + params are DB-driven.
|
|
64
|
+
// Adding a new chain is a DB INSERT, not a code change.
|
|
65
|
+
let encodingParams: EncodingParams = {};
|
|
66
|
+
try {
|
|
67
|
+
encodingParams = JSON.parse(method.encodingParams ?? "{}");
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error(`Invalid encoding_params JSON for chain ${chainId}: ${method.encodingParams}`);
|
|
82
70
|
}
|
|
71
|
+
const address = deriveAddress(method.xpub, index, method.addressType, encodingParams);
|
|
83
72
|
|
|
84
73
|
// Step 2: Record in immutable log. If this address was already derived by a
|
|
85
74
|
// sibling chain (shared xpub), the unique constraint fires and we retry
|
|
@@ -218,9 +207,12 @@ export function createKeyServerApp(deps: KeyServerDeps): Hono {
|
|
|
218
207
|
expectedAmount: expectedAmount.toString(),
|
|
219
208
|
});
|
|
220
209
|
|
|
221
|
-
// Format display amount for the client
|
|
222
|
-
const divisor =
|
|
223
|
-
const
|
|
210
|
+
// Format display amount for the client (BigInt-safe, no Number overflow)
|
|
211
|
+
const divisor = 10n ** BigInt(method.decimals);
|
|
212
|
+
const whole = expectedAmount / divisor;
|
|
213
|
+
const frac = expectedAmount % divisor;
|
|
214
|
+
const fracStr = frac.toString().padStart(method.decimals, "0").slice(0, 8).replace(/0+$/, "");
|
|
215
|
+
const displayAmount = `${whole}${fracStr ? `.${fracStr}` : ""} ${token}`;
|
|
224
216
|
|
|
225
217
|
return c.json(
|
|
226
218
|
{
|
|
@@ -329,6 +321,7 @@ export function createKeyServerApp(deps: KeyServerDeps): Hono {
|
|
|
329
321
|
display_name?: string;
|
|
330
322
|
oracle_address?: string;
|
|
331
323
|
address_type?: string;
|
|
324
|
+
encoding_params?: Record<string, string>;
|
|
332
325
|
icon_url?: string;
|
|
333
326
|
display_order?: number;
|
|
334
327
|
}>();
|
|
@@ -355,6 +348,16 @@ export function createKeyServerApp(deps: KeyServerDeps): Hono {
|
|
|
355
348
|
);
|
|
356
349
|
}
|
|
357
350
|
|
|
351
|
+
// Validate encoding_params match address_type requirements
|
|
352
|
+
const addrType = body.address_type ?? "evm";
|
|
353
|
+
const encParams = body.encoding_params ?? {};
|
|
354
|
+
if (addrType === "bech32" && !encParams.hrp) {
|
|
355
|
+
return c.json({ error: "bech32 address_type requires encoding_params.hrp" }, 400);
|
|
356
|
+
}
|
|
357
|
+
if (addrType === "p2pkh" && !encParams.version) {
|
|
358
|
+
return c.json({ error: "p2pkh address_type requires encoding_params.version" }, 400);
|
|
359
|
+
}
|
|
360
|
+
|
|
358
361
|
// Upsert the payment method
|
|
359
362
|
await deps.methodStore.upsert({
|
|
360
363
|
id: body.id,
|
|
@@ -371,6 +374,7 @@ export function createKeyServerApp(deps: KeyServerDeps): Hono {
|
|
|
371
374
|
oracleAddress: body.oracle_address ?? null,
|
|
372
375
|
xpub: body.xpub,
|
|
373
376
|
addressType: body.address_type ?? "evm",
|
|
377
|
+
encodingParams: JSON.stringify(body.encoding_params ?? {}),
|
|
374
378
|
confirmations: body.confirmations ?? 6,
|
|
375
379
|
});
|
|
376
380
|
|
|
@@ -17,6 +17,7 @@ export interface PaymentMethodRecord {
|
|
|
17
17
|
oracleAddress: string | null;
|
|
18
18
|
xpub: string | null;
|
|
19
19
|
addressType: string;
|
|
20
|
+
encodingParams: string;
|
|
20
21
|
confirmations: number;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -89,6 +90,7 @@ export class DrizzlePaymentMethodStore implements IPaymentMethodStore {
|
|
|
89
90
|
oracleAddress: method.oracleAddress,
|
|
90
91
|
xpub: method.xpub,
|
|
91
92
|
addressType: method.addressType,
|
|
93
|
+
encodingParams: method.encodingParams,
|
|
92
94
|
confirmations: method.confirmations,
|
|
93
95
|
})
|
|
94
96
|
.onConflictDoUpdate({
|
|
@@ -107,6 +109,7 @@ export class DrizzlePaymentMethodStore implements IPaymentMethodStore {
|
|
|
107
109
|
oracleAddress: method.oracleAddress,
|
|
108
110
|
xpub: method.xpub,
|
|
109
111
|
addressType: method.addressType,
|
|
112
|
+
encodingParams: method.encodingParams,
|
|
110
113
|
confirmations: method.confirmations,
|
|
111
114
|
},
|
|
112
115
|
});
|
|
@@ -148,6 +151,7 @@ function toRecord(row: typeof paymentMethods.$inferSelect): PaymentMethodRecord
|
|
|
148
151
|
oracleAddress: row.oracleAddress,
|
|
149
152
|
xpub: row.xpub,
|
|
150
153
|
addressType: row.addressType,
|
|
154
|
+
encodingParams: row.encodingParams,
|
|
151
155
|
confirmations: row.confirmations,
|
|
152
156
|
};
|
|
153
157
|
}
|
package/src/db/schema/crypto.ts
CHANGED
|
@@ -83,6 +83,7 @@ export const paymentMethods = pgTable("payment_methods", {
|
|
|
83
83
|
oracleAddress: text("oracle_address"), // Chainlink feed address for price (null = 1:1 stablecoin)
|
|
84
84
|
xpub: text("xpub"), // HD wallet extended public key for deposit address derivation
|
|
85
85
|
addressType: text("address_type").notNull().default("evm"), // "bech32" (BTC/LTC), "p2pkh" (DOGE), "evm" (ETH/ERC20)
|
|
86
|
+
encodingParams: text("encoding_params").notNull().default("{}"), // JSON: {"hrp":"bc"}, {"version":"0x1e"}, etc.
|
|
86
87
|
confirmations: integer("confirmations").notNull().default(1),
|
|
87
88
|
nextIndex: integer("next_index").notNull().default(0), // atomic derivation counter, never reuses
|
|
88
89
|
createdAt: text("created_at").notNull().default(sql`(now())`),
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { HDKey } from "@scure/bip32";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { deriveBtcAddress, deriveBtcTreasury } from "../address-gen.js";
|
|
4
|
-
function makeTestXpub() {
|
|
5
|
-
const seed = new Uint8Array(32);
|
|
6
|
-
seed[0] = 1;
|
|
7
|
-
const master = HDKey.fromMasterSeed(seed);
|
|
8
|
-
return master.derive("m/44'/0'/0'").publicExtendedKey;
|
|
9
|
-
}
|
|
10
|
-
const TEST_XPUB = makeTestXpub();
|
|
11
|
-
describe("deriveBtcAddress", () => {
|
|
12
|
-
it("derives a valid bech32 address", () => {
|
|
13
|
-
const addr = deriveBtcAddress(TEST_XPUB, 0);
|
|
14
|
-
expect(addr).toMatch(/^bc1q[a-z0-9]+$/);
|
|
15
|
-
});
|
|
16
|
-
it("derives different addresses for different indices", () => {
|
|
17
|
-
const a = deriveBtcAddress(TEST_XPUB, 0);
|
|
18
|
-
const b = deriveBtcAddress(TEST_XPUB, 1);
|
|
19
|
-
expect(a).not.toBe(b);
|
|
20
|
-
});
|
|
21
|
-
it("is deterministic", () => {
|
|
22
|
-
const a = deriveBtcAddress(TEST_XPUB, 42);
|
|
23
|
-
const b = deriveBtcAddress(TEST_XPUB, 42);
|
|
24
|
-
expect(a).toBe(b);
|
|
25
|
-
});
|
|
26
|
-
it("uses tb prefix for testnet/regtest", () => {
|
|
27
|
-
const addr = deriveBtcAddress(TEST_XPUB, 0, "testnet");
|
|
28
|
-
expect(addr).toMatch(/^tb1q[a-z0-9]+$/);
|
|
29
|
-
});
|
|
30
|
-
it("rejects negative index", () => {
|
|
31
|
-
expect(() => deriveBtcAddress(TEST_XPUB, -1)).toThrow("Invalid");
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
describe("deriveBtcTreasury", () => {
|
|
35
|
-
it("derives a valid bech32 address", () => {
|
|
36
|
-
const addr = deriveBtcTreasury(TEST_XPUB);
|
|
37
|
-
expect(addr).toMatch(/^bc1q[a-z0-9]+$/);
|
|
38
|
-
});
|
|
39
|
-
it("differs from deposit address at index 0", () => {
|
|
40
|
-
const deposit = deriveBtcAddress(TEST_XPUB, 0);
|
|
41
|
-
const treasury = deriveBtcTreasury(TEST_XPUB);
|
|
42
|
-
expect(deposit).not.toBe(treasury);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/** Supported UTXO chains for bech32 address derivation. */
|
|
2
|
-
export type UtxoChain = "bitcoin" | "litecoin";
|
|
3
|
-
/** Supported network types. */
|
|
4
|
-
export type UtxoNetwork = "mainnet" | "testnet" | "regtest";
|
|
5
|
-
/**
|
|
6
|
-
* Derive a native segwit (bech32) deposit address from an xpub at a given index.
|
|
7
|
-
* Works for BTC (bc1q...) and LTC (ltc1q...) — same HASH160 + bech32 encoding.
|
|
8
|
-
* Path: xpub / 0 / index (external chain).
|
|
9
|
-
* No private keys involved.
|
|
10
|
-
*/
|
|
11
|
-
export declare function deriveAddress(xpub: string, index: number, network?: UtxoNetwork, chain?: UtxoChain): string;
|
|
12
|
-
/** Derive the treasury address (internal chain, index 0). */
|
|
13
|
-
export declare function deriveTreasury(xpub: string, network?: UtxoNetwork, chain?: UtxoChain): string;
|
|
14
|
-
/**
|
|
15
|
-
* Derive a P2PKH (Base58Check) address for chains without bech32 (e.g., DOGE → D...).
|
|
16
|
-
*/
|
|
17
|
-
export declare function deriveP2pkhAddress(xpub: string, index: number, chain: string, network?: "mainnet" | "testnet"): string;
|
|
18
|
-
/** @deprecated Use `deriveAddress` instead. */
|
|
19
|
-
export declare const deriveBtcAddress: typeof deriveAddress;
|
|
20
|
-
/** @deprecated Use `deriveTreasury` instead. */
|
|
21
|
-
export declare const deriveBtcTreasury: typeof deriveTreasury;
|
|
22
|
-
/** Validate that a string is an xpub (not xprv). */
|
|
23
|
-
export declare function isValidXpub(key: string): boolean;
|