@wopr-network/platform-core 1.34.0 → 1.35.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.
@@ -11,6 +11,10 @@ export type UtxoNetwork = "mainnet" | "testnet" | "regtest";
11
11
  export declare function deriveAddress(xpub: string, index: number, network?: UtxoNetwork, chain?: UtxoChain): string;
12
12
  /** Derive the treasury address (internal chain, index 0). */
13
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;
14
18
  /** @deprecated Use `deriveAddress` instead. */
15
19
  export declare const deriveBtcAddress: typeof deriveAddress;
16
20
  /** @deprecated Use `deriveTreasury` instead. */
@@ -39,6 +39,50 @@ export function deriveTreasury(xpub, network = "mainnet", chain = "bitcoin") {
39
39
  const words = bech32.toWords(hash160);
40
40
  return bech32.encode(prefix, [0, ...words]);
41
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
+ }
42
86
  /** @deprecated Use `deriveAddress` instead. */
43
87
  export const deriveBtcAddress = deriveAddress;
44
88
  /** @deprecated Use `deriveTreasury` instead. */
@@ -25,11 +25,11 @@ export class ChainlinkOracle {
25
25
  maxStalenessMs;
26
26
  constructor(opts) {
27
27
  this.rpc = opts.rpcCall;
28
- this.feeds = { ...FEED_ADDRESSES, ...opts.feedAddresses };
28
+ this.feeds = new Map(Object.entries({ ...FEED_ADDRESSES, ...opts.feedAddresses }));
29
29
  this.maxStalenessMs = opts.maxStalenessMs ?? DEFAULT_MAX_STALENESS_MS;
30
30
  }
31
31
  async getPrice(asset) {
32
- const feedAddress = this.feeds[asset];
32
+ const feedAddress = this.feeds.get(asset);
33
33
  if (!feedAddress)
34
34
  throw new Error(`No price feed for asset: ${asset}`);
35
35
  const result = (await this.rpc("eth_call", [{ to: feedAddress, data: LATEST_ROUND_DATA }, "latest"]));
@@ -1,5 +1,5 @@
1
1
  /** Assets with Chainlink price feeds. */
2
- export type PriceAsset = "ETH" | "BTC";
2
+ export type PriceAsset = string;
3
3
  /** Result from a price oracle query. */
4
4
  export interface PriceResult {
5
5
  /** USD cents per 1 unit of asset (integer). */
@@ -1,4 +1,5 @@
1
1
  import { Credit } from "../../credits/credit.js";
2
+ import { deriveAddress, deriveP2pkhAddress } from "./btc/address-gen.js";
2
3
  import { deriveDepositAddress } from "./evm/address-gen.js";
3
4
  import { centsToNative } from "./oracle/convert.js";
4
5
  export const MIN_CHECKOUT_USD = 10;
@@ -20,11 +21,11 @@ export async function createUnifiedCheckout(deps, method, opts) {
20
21
  if (method.type === "erc20") {
21
22
  return handleErc20(deps, method, opts.tenant, amountUsdCents, opts.amountUsd);
22
23
  }
23
- if (method.token === "ETH") {
24
- return handleNativeEth(deps, method, opts.tenant, amountUsdCents, opts.amountUsd);
24
+ if (method.type === "native" && method.chain === "base") {
25
+ return handleNativeEvm(deps, method, opts.tenant, amountUsdCents, opts.amountUsd);
25
26
  }
26
- if (method.token === "BTC") {
27
- return handleNativeBtc(deps, method, opts.tenant, amountUsdCents, opts.amountUsd);
27
+ if (method.type === "native") {
28
+ return handleNativeUtxo(deps, method, opts.tenant, amountUsdCents, opts.amountUsd);
28
29
  }
29
30
  throw new Error(`Unsupported payment method type: ${method.type}/${method.token}`);
30
31
  }
@@ -39,7 +40,7 @@ async function handleErc20(deps, method, tenant, amountUsdCents, amountUsd) {
39
40
  referenceId: `erc20:${method.chain}:${depositAddress}`,
40
41
  };
41
42
  }
42
- async function handleNativeEth(deps, method, tenant, amountUsdCents, amountUsd) {
43
+ async function handleNativeEvm(deps, method, tenant, amountUsdCents, amountUsd) {
43
44
  const { priceCents } = await deps.oracle.getPrice("ETH");
44
45
  const expectedWei = centsToNative(amountUsdCents, priceCents, 18);
45
46
  const depositAddress = await deriveAndStore(deps, method, tenant, amountUsdCents);
@@ -56,35 +57,47 @@ async function handleNativeEth(deps, method, tenant, amountUsdCents, amountUsd)
56
57
  priceCents,
57
58
  };
58
59
  }
59
- async function handleNativeBtc(deps, _method, tenant, amountUsdCents, amountUsd) {
60
- const { priceCents } = await deps.oracle.getPrice("BTC");
61
- const expectedSats = centsToNative(amountUsdCents, priceCents, 8);
62
- // BTC address derivation uses btcXpub import from btc module
63
- const { deriveBtcAddress } = await import("./btc/address-gen.js");
64
- if (!deps.btcXpub)
65
- throw new Error("BTC payments not configured (no BTC_XPUB)");
60
+ /**
61
+ * Handle native UTXO coins (BTC, LTC, DOGE, BCH, etc.).
62
+ * Uses the xpub from the payment method record (DB-driven).
63
+ * Derives bech32 addresses for BTC/LTC, Base58 P2PKH for DOGE.
64
+ */
65
+ async function handleNativeUtxo(deps, method, tenant, amountUsdCents, amountUsd) {
66
+ const xpub = method.xpub ?? deps.btcXpub;
67
+ if (!xpub)
68
+ throw new Error(`${method.token} payments not configured (no xpub)`);
69
+ const { priceCents } = await deps.oracle.getPrice(method.token);
70
+ const rawAmount = centsToNative(amountUsdCents, priceCents, method.decimals);
66
71
  const maxRetries = 3;
67
72
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
68
73
  const derivationIndex = await deps.chargeStore.getNextDerivationIndex();
69
- const depositAddress = deriveBtcAddress(deps.btcXpub, derivationIndex, "mainnet");
70
- const referenceId = `btc:${depositAddress}`;
74
+ // Derive address by chain type
75
+ let depositAddress;
76
+ if (method.chain === "dogecoin") {
77
+ depositAddress = deriveP2pkhAddress(xpub, derivationIndex, "dogecoin");
78
+ }
79
+ else {
80
+ depositAddress = deriveAddress(xpub, derivationIndex, "mainnet", method.chain);
81
+ }
82
+ const referenceId = `${method.token.toLowerCase()}:${depositAddress}`;
71
83
  try {
72
84
  await deps.chargeStore.createStablecoinCharge({
73
85
  referenceId,
74
86
  tenantId: tenant,
75
87
  amountUsdCents,
76
- chain: "bitcoin",
77
- token: "BTC",
88
+ chain: method.chain,
89
+ token: method.token,
78
90
  depositAddress,
79
91
  derivationIndex,
80
92
  });
81
- const btcAmount = Number(expectedSats) / 100_000_000;
93
+ const divisor = 10 ** method.decimals;
94
+ const displayAmt = (Number(rawAmount) / divisor).toFixed(method.decimals);
82
95
  return {
83
96
  depositAddress,
84
- displayAmount: `${btcAmount.toFixed(8)} BTC`,
97
+ displayAmount: `${displayAmt} ${method.token}`,
85
98
  amountUsd,
86
- token: "BTC",
87
- chain: "bitcoin",
99
+ token: method.token,
100
+ chain: method.chain,
88
101
  referenceId,
89
102
  priceCents,
90
103
  };
@@ -0,0 +1,12 @@
1
+ -- Verified Base mainnet contract addresses (source: basescan.org)
2
+ INSERT INTO "payment_methods" ("id", "type", "token", "chain", "contract_address", "decimals", "display_name", "display_order", "confirmations") VALUES
3
+ ('WETH:base', 'erc20', 'WETH', 'base', '0x4200000000000000000000000000000000000006', 18, 'Wrapped ETH on Base', 4, 1),
4
+ ('cbBTC:base', 'erc20', 'cbBTC', 'base', '0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf', 8, 'Coinbase BTC on Base', 5, 1),
5
+ ('AERO:base', 'erc20', 'AERO', 'base', '0x940181a94A35A4569E4529A3CDfB74e38FD98631', 18, 'Aerodrome on Base', 6, 1),
6
+ ('LINK:base', 'erc20', 'LINK', 'base', '0x88Fb150BDc53A65fe94Dea0c9BA0a6dAf8C6e196', 18, 'Chainlink on Base', 7, 1),
7
+ ('UNI:base', 'erc20', 'UNI', 'base', '0xc3De830EA07524a0761646a6a4e4be0e114a3C83', 18, 'Uniswap on Base', 8, 1),
8
+ ('LTC:litecoin', 'native', 'LTC', 'litecoin', NULL, 8, 'Litecoin', 9, 6),
9
+ ('DOGE:dogecoin', 'native', 'DOGE', 'dogecoin', NULL, 8, 'Dogecoin', 10, 6)
10
+ ON CONFLICT ("id") DO NOTHING;
11
+ -- NOTE: PEPE, SHIB, RENDER not seeded — unverified Base contract addresses.
12
+ -- Add via admin panel after verifying contracts on basescan.org.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.34.0",
3
+ "version": "1.35.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -55,6 +55,55 @@ export function deriveTreasury(xpub: string, network: UtxoNetwork = "mainnet", c
55
55
  return bech32.encode(prefix, [0, ...words]);
56
56
  }
57
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
+
58
107
  /** @deprecated Use `deriveAddress` instead. */
59
108
  export const deriveBtcAddress = deriveAddress;
60
109
 
@@ -36,17 +36,17 @@ export interface ChainlinkOracleOpts {
36
36
  */
37
37
  export class ChainlinkOracle implements IPriceOracle {
38
38
  private readonly rpc: RpcCall;
39
- private readonly feeds: Record<PriceAsset, `0x${string}`>;
39
+ private readonly feeds: Map<string, `0x${string}`>;
40
40
  private readonly maxStalenessMs: number;
41
41
 
42
42
  constructor(opts: ChainlinkOracleOpts) {
43
43
  this.rpc = opts.rpcCall;
44
- this.feeds = { ...FEED_ADDRESSES, ...opts.feedAddresses };
44
+ this.feeds = new Map(Object.entries({ ...FEED_ADDRESSES, ...opts.feedAddresses })) as Map<string, `0x${string}`>;
45
45
  this.maxStalenessMs = opts.maxStalenessMs ?? DEFAULT_MAX_STALENESS_MS;
46
46
  }
47
47
 
48
48
  async getPrice(asset: PriceAsset): Promise<PriceResult> {
49
- const feedAddress = this.feeds[asset];
49
+ const feedAddress = this.feeds.get(asset);
50
50
  if (!feedAddress) throw new Error(`No price feed for asset: ${asset}`);
51
51
 
52
52
  const result = (await this.rpc("eth_call", [{ to: feedAddress, data: LATEST_ROUND_DATA }, "latest"])) as string;
@@ -1,5 +1,5 @@
1
1
  /** Assets with Chainlink price feeds. */
2
- export type PriceAsset = "ETH" | "BTC";
2
+ export type PriceAsset = string;
3
3
 
4
4
  /** Result from a price oracle query. */
5
5
  export interface PriceResult {
@@ -1,4 +1,5 @@
1
1
  import { Credit } from "../../credits/credit.js";
2
+ import { deriveAddress, deriveP2pkhAddress } from "./btc/address-gen.js";
2
3
  import type { ICryptoChargeRepository } from "./charge-store.js";
3
4
  import { deriveDepositAddress } from "./evm/address-gen.js";
4
5
  import { centsToNative } from "./oracle/convert.js";
@@ -50,11 +51,11 @@ export async function createUnifiedCheckout(
50
51
  if (method.type === "erc20") {
51
52
  return handleErc20(deps, method, opts.tenant, amountUsdCents, opts.amountUsd);
52
53
  }
53
- if (method.token === "ETH") {
54
- return handleNativeEth(deps, method, opts.tenant, amountUsdCents, opts.amountUsd);
54
+ if (method.type === "native" && method.chain === "base") {
55
+ return handleNativeEvm(deps, method, opts.tenant, amountUsdCents, opts.amountUsd);
55
56
  }
56
- if (method.token === "BTC") {
57
- return handleNativeBtc(deps, method, opts.tenant, amountUsdCents, opts.amountUsd);
57
+ if (method.type === "native") {
58
+ return handleNativeUtxo(deps, method, opts.tenant, amountUsdCents, opts.amountUsd);
58
59
  }
59
60
 
60
61
  throw new Error(`Unsupported payment method type: ${method.type}/${method.token}`);
@@ -79,7 +80,7 @@ async function handleErc20(
79
80
  };
80
81
  }
81
82
 
82
- async function handleNativeEth(
83
+ async function handleNativeEvm(
83
84
  deps: UnifiedCheckoutDeps,
84
85
  method: PaymentMethodRecord,
85
86
  tenant: string,
@@ -105,44 +106,57 @@ async function handleNativeEth(
105
106
  };
106
107
  }
107
108
 
108
- async function handleNativeBtc(
109
+ /**
110
+ * Handle native UTXO coins (BTC, LTC, DOGE, BCH, etc.).
111
+ * Uses the xpub from the payment method record (DB-driven).
112
+ * Derives bech32 addresses for BTC/LTC, Base58 P2PKH for DOGE.
113
+ */
114
+ async function handleNativeUtxo(
109
115
  deps: UnifiedCheckoutDeps,
110
- _method: PaymentMethodRecord,
116
+ method: PaymentMethodRecord,
111
117
  tenant: string,
112
118
  amountUsdCents: number,
113
119
  amountUsd: number,
114
120
  ): Promise<UnifiedCheckoutResult> {
115
- const { priceCents } = await deps.oracle.getPrice("BTC");
116
- const expectedSats = centsToNative(amountUsdCents, priceCents, 8);
121
+ const xpub = method.xpub ?? deps.btcXpub;
122
+ if (!xpub) throw new Error(`${method.token} payments not configured (no xpub)`);
117
123
 
118
- // BTC address derivation uses btcXpub — import from btc module
119
- const { deriveBtcAddress } = await import("./btc/address-gen.js");
120
- if (!deps.btcXpub) throw new Error("BTC payments not configured (no BTC_XPUB)");
124
+ const { priceCents } = await deps.oracle.getPrice(method.token);
125
+ const rawAmount = centsToNative(amountUsdCents, priceCents, method.decimals);
121
126
 
122
127
  const maxRetries = 3;
123
128
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
124
129
  const derivationIndex = await deps.chargeStore.getNextDerivationIndex();
125
- const depositAddress = deriveBtcAddress(deps.btcXpub, derivationIndex, "mainnet");
126
- const referenceId = `btc:${depositAddress}`;
130
+
131
+ // Derive address by chain type
132
+ let depositAddress: string;
133
+ if (method.chain === "dogecoin") {
134
+ depositAddress = deriveP2pkhAddress(xpub, derivationIndex, "dogecoin");
135
+ } else {
136
+ depositAddress = deriveAddress(xpub, derivationIndex, "mainnet", method.chain as "bitcoin" | "litecoin");
137
+ }
138
+
139
+ const referenceId = `${method.token.toLowerCase()}:${depositAddress}`;
127
140
 
128
141
  try {
129
142
  await deps.chargeStore.createStablecoinCharge({
130
143
  referenceId,
131
144
  tenantId: tenant,
132
145
  amountUsdCents,
133
- chain: "bitcoin",
134
- token: "BTC",
146
+ chain: method.chain,
147
+ token: method.token,
135
148
  depositAddress,
136
149
  derivationIndex,
137
150
  });
138
151
 
139
- const btcAmount = Number(expectedSats) / 100_000_000;
152
+ const divisor = 10 ** method.decimals;
153
+ const displayAmt = (Number(rawAmount) / divisor).toFixed(method.decimals);
140
154
  return {
141
155
  depositAddress,
142
- displayAmount: `${btcAmount.toFixed(8)} BTC`,
156
+ displayAmount: `${displayAmt} ${method.token}`,
143
157
  amountUsd,
144
- token: "BTC",
145
- chain: "bitcoin",
158
+ token: method.token,
159
+ chain: method.chain,
146
160
  referenceId,
147
161
  priceCents,
148
162
  };