@wopr-network/platform-core 1.32.0 → 1.34.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/btc/address-gen.d.ts +15 -4
- package/dist/billing/crypto/btc/address-gen.js +31 -8
- package/dist/billing/crypto/btc/index.d.ts +2 -1
- package/dist/billing/crypto/btc/index.js +1 -1
- package/dist/fleet/init-fleet-updater.d.ts +2 -0
- package/dist/fleet/init-fleet-updater.js +23 -4
- package/package.json +1 -1
- package/src/billing/crypto/btc/address-gen.ts +42 -10
- package/src/billing/crypto/btc/index.ts +2 -1
- package/src/fleet/init-fleet-updater.ts +30 -3
|
@@ -1,8 +1,19 @@
|
|
|
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";
|
|
1
5
|
/**
|
|
2
|
-
* Derive a native segwit (bech32
|
|
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.
|
|
3
8
|
* Path: xpub / 0 / index (external chain).
|
|
4
9
|
* No private keys involved.
|
|
5
10
|
*/
|
|
6
|
-
export declare function
|
|
7
|
-
/** Derive the
|
|
8
|
-
export declare function
|
|
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
|
+
/** @deprecated Use `deriveAddress` instead. */
|
|
15
|
+
export declare const deriveBtcAddress: typeof deriveAddress;
|
|
16
|
+
/** @deprecated Use `deriveTreasury` instead. */
|
|
17
|
+
export declare const deriveBtcTreasury: typeof deriveTreasury;
|
|
18
|
+
/** Validate that a string is an xpub (not xprv). */
|
|
19
|
+
export declare function isValidXpub(key: string): boolean;
|
|
@@ -2,33 +2,56 @@ import { ripemd160 } from "@noble/hashes/legacy.js";
|
|
|
2
2
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
3
3
|
import { bech32 } from "@scure/base";
|
|
4
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
|
+
}
|
|
5
13
|
/**
|
|
6
|
-
* Derive a native segwit (bech32
|
|
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.
|
|
7
16
|
* Path: xpub / 0 / index (external chain).
|
|
8
17
|
* No private keys involved.
|
|
9
18
|
*/
|
|
10
|
-
export function
|
|
19
|
+
export function deriveAddress(xpub, index, network = "mainnet", chain = "bitcoin") {
|
|
11
20
|
if (!Number.isInteger(index) || index < 0)
|
|
12
21
|
throw new Error(`Invalid derivation index: ${index}`);
|
|
13
22
|
const master = HDKey.fromExtendedKey(xpub);
|
|
14
23
|
const child = master.deriveChild(0).deriveChild(index);
|
|
15
24
|
if (!child.publicKey)
|
|
16
25
|
throw new Error("Failed to derive public key");
|
|
17
|
-
// HASH160 = RIPEMD160(SHA256(compressedPubKey))
|
|
18
26
|
const hash160 = ripemd160(sha256(child.publicKey));
|
|
19
|
-
|
|
20
|
-
const prefix = network === "mainnet" ? "bc" : "tb";
|
|
27
|
+
const prefix = getBech32Prefix(chain, network);
|
|
21
28
|
const words = bech32.toWords(hash160);
|
|
22
29
|
return bech32.encode(prefix, [0, ...words]);
|
|
23
30
|
}
|
|
24
|
-
/** Derive the
|
|
25
|
-
export function
|
|
31
|
+
/** Derive the treasury address (internal chain, index 0). */
|
|
32
|
+
export function deriveTreasury(xpub, network = "mainnet", chain = "bitcoin") {
|
|
26
33
|
const master = HDKey.fromExtendedKey(xpub);
|
|
27
34
|
const child = master.deriveChild(1).deriveChild(0); // internal chain
|
|
28
35
|
if (!child.publicKey)
|
|
29
36
|
throw new Error("Failed to derive public key");
|
|
30
37
|
const hash160 = ripemd160(sha256(child.publicKey));
|
|
31
|
-
const prefix = network
|
|
38
|
+
const prefix = getBech32Prefix(chain, network);
|
|
32
39
|
const words = bech32.toWords(hash160);
|
|
33
40
|
return bech32.encode(prefix, [0, ...words]);
|
|
34
41
|
}
|
|
42
|
+
/** @deprecated Use `deriveAddress` instead. */
|
|
43
|
+
export const deriveBtcAddress = deriveAddress;
|
|
44
|
+
/** @deprecated Use `deriveTreasury` instead. */
|
|
45
|
+
export const deriveBtcTreasury = deriveTreasury;
|
|
46
|
+
/** Validate that a string is an xpub (not xprv). */
|
|
47
|
+
export function isValidXpub(key) {
|
|
48
|
+
if (!key.startsWith("xpub"))
|
|
49
|
+
return false;
|
|
50
|
+
try {
|
|
51
|
+
HDKey.fromExtendedKey(key);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export type { UtxoChain, UtxoNetwork } from "./address-gen.js";
|
|
2
|
+
export { deriveAddress, deriveBtcAddress, deriveBtcTreasury, deriveTreasury } from "./address-gen.js";
|
|
2
3
|
export type { BtcCheckoutDeps, BtcCheckoutResult } from "./checkout.js";
|
|
3
4
|
export { createBtcCheckout, MIN_BTC_USD } from "./checkout.js";
|
|
4
5
|
export { centsToSats, loadBitcoindConfig, satsToCents } from "./config.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { deriveBtcAddress, deriveBtcTreasury } from "./address-gen.js";
|
|
1
|
+
export { deriveAddress, deriveBtcAddress, deriveBtcTreasury, deriveTreasury } 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";
|
|
@@ -40,6 +40,8 @@ export interface FleetUpdaterConfig {
|
|
|
40
40
|
configRepo?: ITenantUpdateConfigRepository;
|
|
41
41
|
/** Optional fleet event emitter. When provided, bot.updated / bot.update_failed events are emitted. */
|
|
42
42
|
eventEmitter?: FleetEventEmitter;
|
|
43
|
+
/** Called with manual-mode tenant IDs when a new image is available but they are excluded from rollout. */
|
|
44
|
+
onManualTenantsSkipped?: (tenantIds: string[]) => void;
|
|
43
45
|
}
|
|
44
46
|
export interface FleetUpdaterHandle {
|
|
45
47
|
poller: ImagePoller;
|
|
@@ -30,7 +30,7 @@ import { VolumeSnapshotManager } from "./volume-snapshot-manager.js";
|
|
|
30
30
|
* @param config - Optional pipeline configuration
|
|
31
31
|
*/
|
|
32
32
|
export function initFleetUpdater(docker, fleet, profileStore, profileRepo, config = {}) {
|
|
33
|
-
const { strategy: strategyType = "rolling-wave", strategyOptions, snapshotDir = "/data/fleet/snapshots", onBotUpdated, onRolloutComplete, configRepo, eventEmitter: configEventEmitter, } = config;
|
|
33
|
+
const { strategy: strategyType = "rolling-wave", strategyOptions, snapshotDir = "/data/fleet/snapshots", onBotUpdated, onRolloutComplete, configRepo, eventEmitter: configEventEmitter, onManualTenantsSkipped, } = config;
|
|
34
34
|
const emitter = configEventEmitter ?? new FleetEventEmitter();
|
|
35
35
|
const poller = new ImagePoller(docker, profileStore);
|
|
36
36
|
const updater = new ContainerUpdater(docker, profileStore, fleet, poller);
|
|
@@ -42,17 +42,36 @@ export function initFleetUpdater(docker, fleet, profileStore, profileRepo, confi
|
|
|
42
42
|
strategy,
|
|
43
43
|
getUpdatableProfiles: async () => {
|
|
44
44
|
const profiles = await profileRepo.list();
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// Separate profiles by updatePolicy
|
|
46
|
+
const manualPolicyIds = [];
|
|
47
|
+
const nonManualPolicy = profiles.filter((p) => {
|
|
48
|
+
if (p.updatePolicy === "manual") {
|
|
49
|
+
manualPolicyIds.push(p.tenantId);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
});
|
|
54
|
+
if (!configRepo) {
|
|
55
|
+
if (manualPolicyIds.length > 0 && onManualTenantsSkipped) {
|
|
56
|
+
onManualTenantsSkipped([...new Set(manualPolicyIds)]);
|
|
57
|
+
}
|
|
47
58
|
return nonManualPolicy;
|
|
59
|
+
}
|
|
48
60
|
// Filter out tenants whose per-tenant config is set to manual
|
|
61
|
+
const configManualIds = [];
|
|
49
62
|
const results = await Promise.all(nonManualPolicy.map(async (p) => {
|
|
50
63
|
const tenantCfg = await configRepo.get(p.tenantId);
|
|
51
64
|
// If tenant has an explicit config with mode=manual, exclude
|
|
52
|
-
if (tenantCfg && tenantCfg.mode === "manual")
|
|
65
|
+
if (tenantCfg && tenantCfg.mode === "manual") {
|
|
66
|
+
configManualIds.push(p.tenantId);
|
|
53
67
|
return null;
|
|
68
|
+
}
|
|
54
69
|
return p;
|
|
55
70
|
}));
|
|
71
|
+
const allManualIds = [...manualPolicyIds, ...configManualIds];
|
|
72
|
+
if (allManualIds.length > 0 && onManualTenantsSkipped) {
|
|
73
|
+
onManualTenantsSkipped([...new Set(allManualIds)]);
|
|
74
|
+
}
|
|
56
75
|
return results.filter((p) => p !== null);
|
|
57
76
|
},
|
|
58
77
|
onBotUpdated: (result) => {
|
package/package.json
CHANGED
|
@@ -3,15 +3,33 @@ import { sha256 } from "@noble/hashes/sha2.js";
|
|
|
3
3
|
import { bech32 } from "@scure/base";
|
|
4
4
|
import { HDKey } from "@scure/bip32";
|
|
5
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
|
+
|
|
6
22
|
/**
|
|
7
|
-
* Derive a native segwit (bech32
|
|
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.
|
|
8
25
|
* Path: xpub / 0 / index (external chain).
|
|
9
26
|
* No private keys involved.
|
|
10
27
|
*/
|
|
11
|
-
export function
|
|
28
|
+
export function deriveAddress(
|
|
12
29
|
xpub: string,
|
|
13
30
|
index: number,
|
|
14
|
-
network:
|
|
31
|
+
network: UtxoNetwork = "mainnet",
|
|
32
|
+
chain: UtxoChain = "bitcoin",
|
|
15
33
|
): string {
|
|
16
34
|
if (!Number.isInteger(index) || index < 0) throw new Error(`Invalid derivation index: ${index}`);
|
|
17
35
|
|
|
@@ -19,23 +37,37 @@ export function deriveBtcAddress(
|
|
|
19
37
|
const child = master.deriveChild(0).deriveChild(index);
|
|
20
38
|
if (!child.publicKey) throw new Error("Failed to derive public key");
|
|
21
39
|
|
|
22
|
-
// HASH160 = RIPEMD160(SHA256(compressedPubKey))
|
|
23
40
|
const hash160 = ripemd160(sha256(child.publicKey));
|
|
24
|
-
|
|
25
|
-
// Bech32 encode: witness version 0 + 20-byte hash
|
|
26
|
-
const prefix = network === "mainnet" ? "bc" : "tb";
|
|
41
|
+
const prefix = getBech32Prefix(chain, network);
|
|
27
42
|
const words = bech32.toWords(hash160);
|
|
28
43
|
return bech32.encode(prefix, [0, ...words]);
|
|
29
44
|
}
|
|
30
45
|
|
|
31
|
-
/** Derive the
|
|
32
|
-
export function
|
|
46
|
+
/** Derive the treasury address (internal chain, index 0). */
|
|
47
|
+
export function deriveTreasury(xpub: string, network: UtxoNetwork = "mainnet", chain: UtxoChain = "bitcoin"): string {
|
|
33
48
|
const master = HDKey.fromExtendedKey(xpub);
|
|
34
49
|
const child = master.deriveChild(1).deriveChild(0); // internal chain
|
|
35
50
|
if (!child.publicKey) throw new Error("Failed to derive public key");
|
|
36
51
|
|
|
37
52
|
const hash160 = ripemd160(sha256(child.publicKey));
|
|
38
|
-
const prefix = network
|
|
53
|
+
const prefix = getBech32Prefix(chain, network);
|
|
39
54
|
const words = bech32.toWords(hash160);
|
|
40
55
|
return bech32.encode(prefix, [0, ...words]);
|
|
41
56
|
}
|
|
57
|
+
|
|
58
|
+
/** @deprecated Use `deriveAddress` instead. */
|
|
59
|
+
export const deriveBtcAddress = deriveAddress;
|
|
60
|
+
|
|
61
|
+
/** @deprecated Use `deriveTreasury` instead. */
|
|
62
|
+
export const deriveBtcTreasury = deriveTreasury;
|
|
63
|
+
|
|
64
|
+
/** Validate that a string is an xpub (not xprv). */
|
|
65
|
+
export function isValidXpub(key: string): boolean {
|
|
66
|
+
if (!key.startsWith("xpub")) return false;
|
|
67
|
+
try {
|
|
68
|
+
HDKey.fromExtendedKey(key);
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export type { UtxoChain, UtxoNetwork } from "./address-gen.js";
|
|
2
|
+
export { deriveAddress, deriveBtcAddress, deriveBtcTreasury, deriveTreasury } from "./address-gen.js";
|
|
2
3
|
export type { BtcCheckoutDeps, BtcCheckoutResult } from "./checkout.js";
|
|
3
4
|
export { createBtcCheckout, MIN_BTC_USD } from "./checkout.js";
|
|
4
5
|
export { centsToSats, loadBitcoindConfig, satsToCents } from "./config.js";
|
|
@@ -39,6 +39,8 @@ export interface FleetUpdaterConfig {
|
|
|
39
39
|
configRepo?: ITenantUpdateConfigRepository;
|
|
40
40
|
/** Optional fleet event emitter. When provided, bot.updated / bot.update_failed events are emitted. */
|
|
41
41
|
eventEmitter?: FleetEventEmitter;
|
|
42
|
+
/** Called with manual-mode tenant IDs when a new image is available but they are excluded from rollout. */
|
|
43
|
+
onManualTenantsSkipped?: (tenantIds: string[]) => void;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export interface FleetUpdaterHandle {
|
|
@@ -79,6 +81,7 @@ export function initFleetUpdater(
|
|
|
79
81
|
onRolloutComplete,
|
|
80
82
|
configRepo,
|
|
81
83
|
eventEmitter: configEventEmitter,
|
|
84
|
+
onManualTenantsSkipped,
|
|
82
85
|
} = config;
|
|
83
86
|
|
|
84
87
|
const emitter = configEventEmitter ?? new FleetEventEmitter();
|
|
@@ -94,19 +97,43 @@ export function initFleetUpdater(
|
|
|
94
97
|
strategy,
|
|
95
98
|
getUpdatableProfiles: async () => {
|
|
96
99
|
const profiles = await profileRepo.list();
|
|
97
|
-
const nonManualPolicy = profiles.filter((p) => p.updatePolicy !== "manual");
|
|
98
100
|
|
|
99
|
-
|
|
101
|
+
// Separate profiles by updatePolicy
|
|
102
|
+
const manualPolicyIds: string[] = [];
|
|
103
|
+
const nonManualPolicy = profiles.filter((p) => {
|
|
104
|
+
if (p.updatePolicy === "manual") {
|
|
105
|
+
manualPolicyIds.push(p.tenantId);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!configRepo) {
|
|
112
|
+
if (manualPolicyIds.length > 0 && onManualTenantsSkipped) {
|
|
113
|
+
onManualTenantsSkipped([...new Set(manualPolicyIds)]);
|
|
114
|
+
}
|
|
115
|
+
return nonManualPolicy;
|
|
116
|
+
}
|
|
100
117
|
|
|
101
118
|
// Filter out tenants whose per-tenant config is set to manual
|
|
119
|
+
const configManualIds: string[] = [];
|
|
102
120
|
const results = await Promise.all(
|
|
103
121
|
nonManualPolicy.map(async (p) => {
|
|
104
122
|
const tenantCfg = await configRepo.get(p.tenantId);
|
|
105
123
|
// If tenant has an explicit config with mode=manual, exclude
|
|
106
|
-
if (tenantCfg && tenantCfg.mode === "manual")
|
|
124
|
+
if (tenantCfg && tenantCfg.mode === "manual") {
|
|
125
|
+
configManualIds.push(p.tenantId);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
107
128
|
return p;
|
|
108
129
|
}),
|
|
109
130
|
);
|
|
131
|
+
|
|
132
|
+
const allManualIds = [...manualPolicyIds, ...configManualIds];
|
|
133
|
+
if (allManualIds.length > 0 && onManualTenantsSkipped) {
|
|
134
|
+
onManualTenantsSkipped([...new Set(allManualIds)]);
|
|
135
|
+
}
|
|
136
|
+
|
|
110
137
|
return results.filter((p) => p !== null);
|
|
111
138
|
},
|
|
112
139
|
onBotUpdated: (result) => {
|