@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
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { ripemd160 } from "@noble/hashes/legacy.js";
|
|
2
|
-
import { sha256 } from "@noble/hashes/sha2.js";
|
|
3
|
-
import { bech32 } from "@scure/base";
|
|
4
|
-
import { HDKey } from "@scure/bip32";
|
|
5
|
-
/** Bech32 HRP (human-readable part) by chain and network. */
|
|
6
|
-
const BECH32_PREFIX = {
|
|
7
|
-
bitcoin: { mainnet: "bc", testnet: "tb", regtest: "bcrt" },
|
|
8
|
-
litecoin: { mainnet: "ltc", testnet: "tltc", regtest: "rltc" },
|
|
9
|
-
};
|
|
10
|
-
function getBech32Prefix(chain, network) {
|
|
11
|
-
return BECH32_PREFIX[chain][network];
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Derive a native segwit (bech32) deposit address from an xpub at a given index.
|
|
15
|
-
* Works for BTC (bc1q...) and LTC (ltc1q...) — same HASH160 + bech32 encoding.
|
|
16
|
-
* Path: xpub / 0 / index (external chain).
|
|
17
|
-
* No private keys involved.
|
|
18
|
-
*/
|
|
19
|
-
export function deriveAddress(xpub, index, network = "mainnet", chain = "bitcoin") {
|
|
20
|
-
if (!Number.isInteger(index) || index < 0)
|
|
21
|
-
throw new Error(`Invalid derivation index: ${index}`);
|
|
22
|
-
const master = HDKey.fromExtendedKey(xpub);
|
|
23
|
-
const child = master.deriveChild(0).deriveChild(index);
|
|
24
|
-
if (!child.publicKey)
|
|
25
|
-
throw new Error("Failed to derive public key");
|
|
26
|
-
const hash160 = ripemd160(sha256(child.publicKey));
|
|
27
|
-
const prefix = getBech32Prefix(chain, network);
|
|
28
|
-
const words = bech32.toWords(hash160);
|
|
29
|
-
return bech32.encode(prefix, [0, ...words]);
|
|
30
|
-
}
|
|
31
|
-
/** Derive the treasury address (internal chain, index 0). */
|
|
32
|
-
export function deriveTreasury(xpub, network = "mainnet", chain = "bitcoin") {
|
|
33
|
-
const master = HDKey.fromExtendedKey(xpub);
|
|
34
|
-
const child = master.deriveChild(1).deriveChild(0); // internal chain
|
|
35
|
-
if (!child.publicKey)
|
|
36
|
-
throw new Error("Failed to derive public key");
|
|
37
|
-
const hash160 = ripemd160(sha256(child.publicKey));
|
|
38
|
-
const prefix = getBech32Prefix(chain, network);
|
|
39
|
-
const words = bech32.toWords(hash160);
|
|
40
|
-
return bech32.encode(prefix, [0, ...words]);
|
|
41
|
-
}
|
|
42
|
-
/** P2PKH version bytes for Base58Check encoding (chains without bech32). */
|
|
43
|
-
const P2PKH_VERSION = {
|
|
44
|
-
dogecoin: { mainnet: 0x1e, testnet: 0x71 },
|
|
45
|
-
};
|
|
46
|
-
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
47
|
-
function base58encode(data) {
|
|
48
|
-
let num = 0n;
|
|
49
|
-
for (const byte of data)
|
|
50
|
-
num = num * 256n + BigInt(byte);
|
|
51
|
-
let encoded = "";
|
|
52
|
-
while (num > 0n) {
|
|
53
|
-
encoded = BASE58_ALPHABET[Number(num % 58n)] + encoded;
|
|
54
|
-
num = num / 58n;
|
|
55
|
-
}
|
|
56
|
-
for (const byte of data) {
|
|
57
|
-
if (byte !== 0)
|
|
58
|
-
break;
|
|
59
|
-
encoded = `1${encoded}`;
|
|
60
|
-
}
|
|
61
|
-
return encoded;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Derive a P2PKH (Base58Check) address for chains without bech32 (e.g., DOGE → D...).
|
|
65
|
-
*/
|
|
66
|
-
export function deriveP2pkhAddress(xpub, index, chain, network = "mainnet") {
|
|
67
|
-
if (!Number.isInteger(index) || index < 0)
|
|
68
|
-
throw new Error(`Invalid derivation index: ${index}`);
|
|
69
|
-
const version = P2PKH_VERSION[chain]?.[network];
|
|
70
|
-
if (version === undefined)
|
|
71
|
-
throw new Error(`No P2PKH version for chain=${chain} network=${network}`);
|
|
72
|
-
const master = HDKey.fromExtendedKey(xpub);
|
|
73
|
-
const child = master.deriveChild(0).deriveChild(index);
|
|
74
|
-
if (!child.publicKey)
|
|
75
|
-
throw new Error("Failed to derive public key");
|
|
76
|
-
const hash160 = ripemd160(sha256(child.publicKey));
|
|
77
|
-
const payload = new Uint8Array(21);
|
|
78
|
-
payload[0] = version;
|
|
79
|
-
payload.set(hash160, 1);
|
|
80
|
-
const checksum = sha256(sha256(payload));
|
|
81
|
-
const full = new Uint8Array(25);
|
|
82
|
-
full.set(payload);
|
|
83
|
-
full.set(checksum.slice(0, 4), 21);
|
|
84
|
-
return base58encode(full);
|
|
85
|
-
}
|
|
86
|
-
/** @deprecated Use `deriveAddress` instead. */
|
|
87
|
-
export const deriveBtcAddress = deriveAddress;
|
|
88
|
-
/** @deprecated Use `deriveTreasury` instead. */
|
|
89
|
-
export const deriveBtcTreasury = deriveTreasury;
|
|
90
|
-
/** Validate that a string is an xpub (not xprv). */
|
|
91
|
-
export function isValidXpub(key) {
|
|
92
|
-
if (!key.startsWith("xpub"))
|
|
93
|
-
return false;
|
|
94
|
-
try {
|
|
95
|
-
HDKey.fromExtendedKey(key);
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { HDKey } from "@scure/bip32";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { deriveDepositAddress, isValidXpub } from "../address-gen.js";
|
|
4
|
-
// Generate a test xpub deterministically
|
|
5
|
-
function makeTestXpub() {
|
|
6
|
-
const seed = new Uint8Array(32);
|
|
7
|
-
seed[0] = 1; // deterministic seed
|
|
8
|
-
const master = HDKey.fromMasterSeed(seed);
|
|
9
|
-
// Derive to m/44'/60'/0' (Ethereum BIP-44 path)
|
|
10
|
-
const account = master.derive("m/44'/60'/0'");
|
|
11
|
-
return account.publicExtendedKey;
|
|
12
|
-
}
|
|
13
|
-
const TEST_XPUB = makeTestXpub();
|
|
14
|
-
describe("deriveDepositAddress", () => {
|
|
15
|
-
it("derives a valid Ethereum address", () => {
|
|
16
|
-
const addr = deriveDepositAddress(TEST_XPUB, 0);
|
|
17
|
-
expect(addr).toMatch(/^0x[0-9a-fA-F]{40}$/);
|
|
18
|
-
});
|
|
19
|
-
it("derives different addresses for different indices", () => {
|
|
20
|
-
const addr0 = deriveDepositAddress(TEST_XPUB, 0);
|
|
21
|
-
const addr1 = deriveDepositAddress(TEST_XPUB, 1);
|
|
22
|
-
expect(addr0).not.toBe(addr1);
|
|
23
|
-
});
|
|
24
|
-
it("is deterministic — same xpub + index = same address", () => {
|
|
25
|
-
const a = deriveDepositAddress(TEST_XPUB, 42);
|
|
26
|
-
const b = deriveDepositAddress(TEST_XPUB, 42);
|
|
27
|
-
expect(a).toBe(b);
|
|
28
|
-
});
|
|
29
|
-
it("returns checksummed address", () => {
|
|
30
|
-
const addr = deriveDepositAddress(TEST_XPUB, 0);
|
|
31
|
-
// Must be a valid 0x-prefixed address
|
|
32
|
-
expect(addr).toMatch(/^0x[0-9a-fA-F]{40}$/);
|
|
33
|
-
// viem's publicKeyToAddress always returns EIP-55 checksummed
|
|
34
|
-
// Verify it's not all-lowercase (checksummed addresses have mixed case)
|
|
35
|
-
const hexPart = addr.slice(2);
|
|
36
|
-
const hasUpperCase = hexPart !== hexPart.toLowerCase();
|
|
37
|
-
const hasLowerCase = hexPart !== hexPart.toUpperCase();
|
|
38
|
-
// At least one of these should be true for a checksummed address
|
|
39
|
-
// (unless the address happens to be all digits, which is extremely rare)
|
|
40
|
-
expect(hasUpperCase || !hexPart.match(/[a-f]/i)).toBe(true);
|
|
41
|
-
expect(hasLowerCase || !hexPart.match(/[a-f]/i)).toBe(true);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
describe("isValidXpub", () => {
|
|
45
|
-
it("accepts valid xpub", () => {
|
|
46
|
-
expect(isValidXpub(TEST_XPUB)).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
it("rejects garbage", () => {
|
|
49
|
-
expect(isValidXpub("not-an-xpub")).toBe(false);
|
|
50
|
-
});
|
|
51
|
-
it("rejects empty string", () => {
|
|
52
|
-
expect(isValidXpub("")).toBe(false);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Derive a deposit address from an xpub at a given BIP-44 index.
|
|
3
|
-
* Path: xpub / 0 / index (external chain / address index).
|
|
4
|
-
* Returns a checksummed Ethereum address. No private keys involved.
|
|
5
|
-
*/
|
|
6
|
-
export declare function deriveDepositAddress(xpub: string, index: number): `0x${string}`;
|
|
7
|
-
/** Validate that a string is an xpub (not xprv). */
|
|
8
|
-
export declare function isValidXpub(key: string): boolean;
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { HDKey } from "@scure/bip32";
|
|
2
|
-
import { publicKeyToAddress } from "viem/accounts";
|
|
3
|
-
/**
|
|
4
|
-
* Derive a deposit address from an xpub at a given BIP-44 index.
|
|
5
|
-
* Path: xpub / 0 / index (external chain / address index).
|
|
6
|
-
* Returns a checksummed Ethereum address. No private keys involved.
|
|
7
|
-
*/
|
|
8
|
-
export function deriveDepositAddress(xpub, index) {
|
|
9
|
-
if (!Number.isInteger(index) || index < 0)
|
|
10
|
-
throw new Error(`Invalid derivation index: ${index}`);
|
|
11
|
-
const master = HDKey.fromExtendedKey(xpub);
|
|
12
|
-
const child = master.deriveChild(0).deriveChild(index);
|
|
13
|
-
if (!child.publicKey)
|
|
14
|
-
throw new Error("Failed to derive public key");
|
|
15
|
-
const hexPubKey = `0x${Array.from(child.publicKey, (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
16
|
-
return publicKeyToAddress(hexPubKey);
|
|
17
|
-
}
|
|
18
|
-
/** Validate that a string is an xpub (not xprv). */
|
|
19
|
-
export function isValidXpub(key) {
|
|
20
|
-
if (!key.startsWith("xpub"))
|
|
21
|
-
return false;
|
|
22
|
-
try {
|
|
23
|
-
HDKey.fromExtendedKey(key);
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { HDKey } from "@scure/bip32";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { deriveBtcAddress, deriveBtcTreasury } from "../address-gen.js";
|
|
4
|
-
|
|
5
|
-
function makeTestXpub(): string {
|
|
6
|
-
const seed = new Uint8Array(32);
|
|
7
|
-
seed[0] = 1;
|
|
8
|
-
const master = HDKey.fromMasterSeed(seed);
|
|
9
|
-
return master.derive("m/44'/0'/0'").publicExtendedKey;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const TEST_XPUB = makeTestXpub();
|
|
13
|
-
|
|
14
|
-
describe("deriveBtcAddress", () => {
|
|
15
|
-
it("derives a valid bech32 address", () => {
|
|
16
|
-
const addr = deriveBtcAddress(TEST_XPUB, 0);
|
|
17
|
-
expect(addr).toMatch(/^bc1q[a-z0-9]+$/);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("derives different addresses for different indices", () => {
|
|
21
|
-
const a = deriveBtcAddress(TEST_XPUB, 0);
|
|
22
|
-
const b = deriveBtcAddress(TEST_XPUB, 1);
|
|
23
|
-
expect(a).not.toBe(b);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("is deterministic", () => {
|
|
27
|
-
const a = deriveBtcAddress(TEST_XPUB, 42);
|
|
28
|
-
const b = deriveBtcAddress(TEST_XPUB, 42);
|
|
29
|
-
expect(a).toBe(b);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("uses tb prefix for testnet/regtest", () => {
|
|
33
|
-
const addr = deriveBtcAddress(TEST_XPUB, 0, "testnet");
|
|
34
|
-
expect(addr).toMatch(/^tb1q[a-z0-9]+$/);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("rejects negative index", () => {
|
|
38
|
-
expect(() => deriveBtcAddress(TEST_XPUB, -1)).toThrow("Invalid");
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe("deriveBtcTreasury", () => {
|
|
43
|
-
it("derives a valid bech32 address", () => {
|
|
44
|
-
const addr = deriveBtcTreasury(TEST_XPUB);
|
|
45
|
-
expect(addr).toMatch(/^bc1q[a-z0-9]+$/);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("differs from deposit address at index 0", () => {
|
|
49
|
-
const deposit = deriveBtcAddress(TEST_XPUB, 0);
|
|
50
|
-
const treasury = deriveBtcTreasury(TEST_XPUB);
|
|
51
|
-
expect(deposit).not.toBe(treasury);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { ripemd160 } from "@noble/hashes/legacy.js";
|
|
2
|
-
import { sha256 } from "@noble/hashes/sha2.js";
|
|
3
|
-
import { bech32 } from "@scure/base";
|
|
4
|
-
import { HDKey } from "@scure/bip32";
|
|
5
|
-
|
|
6
|
-
/** Supported UTXO chains for bech32 address derivation. */
|
|
7
|
-
export type UtxoChain = "bitcoin" | "litecoin";
|
|
8
|
-
|
|
9
|
-
/** Supported network types. */
|
|
10
|
-
export type UtxoNetwork = "mainnet" | "testnet" | "regtest";
|
|
11
|
-
|
|
12
|
-
/** Bech32 HRP (human-readable part) by chain and network. */
|
|
13
|
-
const BECH32_PREFIX = {
|
|
14
|
-
bitcoin: { mainnet: "bc", testnet: "tb", regtest: "bcrt" },
|
|
15
|
-
litecoin: { mainnet: "ltc", testnet: "tltc", regtest: "rltc" },
|
|
16
|
-
} as const;
|
|
17
|
-
|
|
18
|
-
function getBech32Prefix(chain: UtxoChain, network: UtxoNetwork): string {
|
|
19
|
-
return BECH32_PREFIX[chain][network];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Derive a native segwit (bech32) deposit address from an xpub at a given index.
|
|
24
|
-
* Works for BTC (bc1q...) and LTC (ltc1q...) — same HASH160 + bech32 encoding.
|
|
25
|
-
* Path: xpub / 0 / index (external chain).
|
|
26
|
-
* No private keys involved.
|
|
27
|
-
*/
|
|
28
|
-
export function deriveAddress(
|
|
29
|
-
xpub: string,
|
|
30
|
-
index: number,
|
|
31
|
-
network: UtxoNetwork = "mainnet",
|
|
32
|
-
chain: UtxoChain = "bitcoin",
|
|
33
|
-
): string {
|
|
34
|
-
if (!Number.isInteger(index) || index < 0) throw new Error(`Invalid derivation index: ${index}`);
|
|
35
|
-
|
|
36
|
-
const master = HDKey.fromExtendedKey(xpub);
|
|
37
|
-
const child = master.deriveChild(0).deriveChild(index);
|
|
38
|
-
if (!child.publicKey) throw new Error("Failed to derive public key");
|
|
39
|
-
|
|
40
|
-
const hash160 = ripemd160(sha256(child.publicKey));
|
|
41
|
-
const prefix = getBech32Prefix(chain, network);
|
|
42
|
-
const words = bech32.toWords(hash160);
|
|
43
|
-
return bech32.encode(prefix, [0, ...words]);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Derive the treasury address (internal chain, index 0). */
|
|
47
|
-
export function deriveTreasury(xpub: string, network: UtxoNetwork = "mainnet", chain: UtxoChain = "bitcoin"): string {
|
|
48
|
-
const master = HDKey.fromExtendedKey(xpub);
|
|
49
|
-
const child = master.deriveChild(1).deriveChild(0); // internal chain
|
|
50
|
-
if (!child.publicKey) throw new Error("Failed to derive public key");
|
|
51
|
-
|
|
52
|
-
const hash160 = ripemd160(sha256(child.publicKey));
|
|
53
|
-
const prefix = getBech32Prefix(chain, network);
|
|
54
|
-
const words = bech32.toWords(hash160);
|
|
55
|
-
return bech32.encode(prefix, [0, ...words]);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** P2PKH version bytes for Base58Check encoding (chains without bech32). */
|
|
59
|
-
const P2PKH_VERSION: Record<string, Record<string, number>> = {
|
|
60
|
-
dogecoin: { mainnet: 0x1e, testnet: 0x71 },
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
64
|
-
function base58encode(data: Uint8Array): string {
|
|
65
|
-
let num = 0n;
|
|
66
|
-
for (const byte of data) num = num * 256n + BigInt(byte);
|
|
67
|
-
let encoded = "";
|
|
68
|
-
while (num > 0n) {
|
|
69
|
-
encoded = BASE58_ALPHABET[Number(num % 58n)] + encoded;
|
|
70
|
-
num = num / 58n;
|
|
71
|
-
}
|
|
72
|
-
for (const byte of data) {
|
|
73
|
-
if (byte !== 0) break;
|
|
74
|
-
encoded = `1${encoded}`;
|
|
75
|
-
}
|
|
76
|
-
return encoded;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Derive a P2PKH (Base58Check) address for chains without bech32 (e.g., DOGE → D...).
|
|
81
|
-
*/
|
|
82
|
-
export function deriveP2pkhAddress(
|
|
83
|
-
xpub: string,
|
|
84
|
-
index: number,
|
|
85
|
-
chain: string,
|
|
86
|
-
network: "mainnet" | "testnet" = "mainnet",
|
|
87
|
-
): string {
|
|
88
|
-
if (!Number.isInteger(index) || index < 0) throw new Error(`Invalid derivation index: ${index}`);
|
|
89
|
-
const version = P2PKH_VERSION[chain]?.[network];
|
|
90
|
-
if (version === undefined) throw new Error(`No P2PKH version for chain=${chain} network=${network}`);
|
|
91
|
-
|
|
92
|
-
const master = HDKey.fromExtendedKey(xpub);
|
|
93
|
-
const child = master.deriveChild(0).deriveChild(index);
|
|
94
|
-
if (!child.publicKey) throw new Error("Failed to derive public key");
|
|
95
|
-
|
|
96
|
-
const hash160 = ripemd160(sha256(child.publicKey));
|
|
97
|
-
const payload = new Uint8Array(21);
|
|
98
|
-
payload[0] = version;
|
|
99
|
-
payload.set(hash160, 1);
|
|
100
|
-
const checksum = sha256(sha256(payload));
|
|
101
|
-
const full = new Uint8Array(25);
|
|
102
|
-
full.set(payload);
|
|
103
|
-
full.set(checksum.slice(0, 4), 21);
|
|
104
|
-
return base58encode(full);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** @deprecated Use `deriveAddress` instead. */
|
|
108
|
-
export const deriveBtcAddress = deriveAddress;
|
|
109
|
-
|
|
110
|
-
/** @deprecated Use `deriveTreasury` instead. */
|
|
111
|
-
export const deriveBtcTreasury = deriveTreasury;
|
|
112
|
-
|
|
113
|
-
/** Validate that a string is an xpub (not xprv). */
|
|
114
|
-
export function isValidXpub(key: string): boolean {
|
|
115
|
-
if (!key.startsWith("xpub")) return false;
|
|
116
|
-
try {
|
|
117
|
-
HDKey.fromExtendedKey(key);
|
|
118
|
-
return true;
|
|
119
|
-
} catch {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { HDKey } from "@scure/bip32";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { deriveDepositAddress, isValidXpub } from "../address-gen.js";
|
|
4
|
-
|
|
5
|
-
// Generate a test xpub deterministically
|
|
6
|
-
function makeTestXpub(): string {
|
|
7
|
-
const seed = new Uint8Array(32);
|
|
8
|
-
seed[0] = 1; // deterministic seed
|
|
9
|
-
const master = HDKey.fromMasterSeed(seed);
|
|
10
|
-
// Derive to m/44'/60'/0' (Ethereum BIP-44 path)
|
|
11
|
-
const account = master.derive("m/44'/60'/0'");
|
|
12
|
-
return account.publicExtendedKey;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const TEST_XPUB = makeTestXpub();
|
|
16
|
-
|
|
17
|
-
describe("deriveDepositAddress", () => {
|
|
18
|
-
it("derives a valid Ethereum address", () => {
|
|
19
|
-
const addr = deriveDepositAddress(TEST_XPUB, 0);
|
|
20
|
-
expect(addr).toMatch(/^0x[0-9a-fA-F]{40}$/);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("derives different addresses for different indices", () => {
|
|
24
|
-
const addr0 = deriveDepositAddress(TEST_XPUB, 0);
|
|
25
|
-
const addr1 = deriveDepositAddress(TEST_XPUB, 1);
|
|
26
|
-
expect(addr0).not.toBe(addr1);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("is deterministic — same xpub + index = same address", () => {
|
|
30
|
-
const a = deriveDepositAddress(TEST_XPUB, 42);
|
|
31
|
-
const b = deriveDepositAddress(TEST_XPUB, 42);
|
|
32
|
-
expect(a).toBe(b);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("returns checksummed address", () => {
|
|
36
|
-
const addr = deriveDepositAddress(TEST_XPUB, 0);
|
|
37
|
-
// Must be a valid 0x-prefixed address
|
|
38
|
-
expect(addr).toMatch(/^0x[0-9a-fA-F]{40}$/);
|
|
39
|
-
// viem's publicKeyToAddress always returns EIP-55 checksummed
|
|
40
|
-
// Verify it's not all-lowercase (checksummed addresses have mixed case)
|
|
41
|
-
const hexPart = addr.slice(2);
|
|
42
|
-
const hasUpperCase = hexPart !== hexPart.toLowerCase();
|
|
43
|
-
const hasLowerCase = hexPart !== hexPart.toUpperCase();
|
|
44
|
-
// At least one of these should be true for a checksummed address
|
|
45
|
-
// (unless the address happens to be all digits, which is extremely rare)
|
|
46
|
-
expect(hasUpperCase || !hexPart.match(/[a-f]/i)).toBe(true);
|
|
47
|
-
expect(hasLowerCase || !hexPart.match(/[a-f]/i)).toBe(true);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe("isValidXpub", () => {
|
|
52
|
-
it("accepts valid xpub", () => {
|
|
53
|
-
expect(isValidXpub(TEST_XPUB)).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("rejects garbage", () => {
|
|
57
|
-
expect(isValidXpub("not-an-xpub")).toBe(false);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("rejects empty string", () => {
|
|
61
|
-
expect(isValidXpub("")).toBe(false);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { HDKey } from "@scure/bip32";
|
|
2
|
-
import { publicKeyToAddress } from "viem/accounts";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Derive a deposit address from an xpub at a given BIP-44 index.
|
|
6
|
-
* Path: xpub / 0 / index (external chain / address index).
|
|
7
|
-
* Returns a checksummed Ethereum address. No private keys involved.
|
|
8
|
-
*/
|
|
9
|
-
export function deriveDepositAddress(xpub: string, index: number): `0x${string}` {
|
|
10
|
-
if (!Number.isInteger(index) || index < 0) throw new Error(`Invalid derivation index: ${index}`);
|
|
11
|
-
const master = HDKey.fromExtendedKey(xpub);
|
|
12
|
-
const child = master.deriveChild(0).deriveChild(index);
|
|
13
|
-
if (!child.publicKey) throw new Error("Failed to derive public key");
|
|
14
|
-
|
|
15
|
-
const hexPubKey =
|
|
16
|
-
`0x${Array.from(child.publicKey, (b) => b.toString(16).padStart(2, "0")).join("")}` as `0x${string}`;
|
|
17
|
-
return publicKeyToAddress(hexPubKey);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Validate that a string is an xpub (not xprv). */
|
|
21
|
-
export function isValidXpub(key: string): boolean {
|
|
22
|
-
if (!key.startsWith("xpub")) return false;
|
|
23
|
-
try {
|
|
24
|
-
HDKey.fromExtendedKey(key);
|
|
25
|
-
return true;
|
|
26
|
-
} catch {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
File without changes
|