From 5079f78b57be70fb47b3e2c46df71d3a6b38ffb3 Mon Sep 17 00:00:00 2001 From: anson Date: Wed, 8 Oct 2025 16:39:07 +0100 Subject: [PATCH 1/9] feat(artillery, auto-topup): implement auto top-up functionality for ledger balance test command: ``` NETWORK=naga-staging bun run artillery:init --auto-topu ``` --- e2e/artillery/src/init.ts | 123 ++++++++++++++++++++++++++++---------- 1 file changed, 93 insertions(+), 30 deletions(-) diff --git a/e2e/artillery/src/init.ts b/e2e/artillery/src/init.ts index d9f4c4c55..61bc53b5e 100644 --- a/e2e/artillery/src/init.ts +++ b/e2e/artillery/src/init.ts @@ -1,20 +1,71 @@ -import '../../src/helper/supressLogs'; import { createAuthManager, storagePlugins, ViemAccountAuthenticator, } from '@lit-protocol/auth'; -import * as StateManager from './StateManager'; import { createLitClient } from '@lit-protocol/lit-client'; -import { getOrCreatePkp } from '../../../e2e/src/helper/pkp-utils'; import * as NetworkManager from '../../../e2e/src/helper/NetworkManager'; +import { getOrCreatePkp } from '../../../e2e/src/helper/pkp-utils'; +import '../../src/helper/supressLogs'; import * as AccountManager from '../src/AccountManager'; +import * as StateManager from './StateManager'; const _network = process.env['NETWORK']; +const AUTO_TOP_UP_FLAG = '--auto-topup'; +const args = process.argv.slice(2); + // CONFIGURATIONS const REJECT_BALANCE_THRESHOLD = 0; const LEDGER_MINIMUM_BALANCE = 10000; +const AUTO_TOP_UP_ENABLED = args.includes(AUTO_TOP_UP_FLAG); +const AUTO_TOP_UP_INTERVAL = 10_000; +const AUTO_TOP_UP_THRESHOLD = LEDGER_MINIMUM_BALANCE; + +if (Number.isNaN(LEDGER_MINIMUM_BALANCE) || LEDGER_MINIMUM_BALANCE < 0) { + throw new Error('❌ LEDGER_MINIMUM_BALANCE must be a non-negative number'); +} + +const ensureLedgerThreshold = async ({ + paymentManager, + accountAddress, + minimumBalance, +}: { + paymentManager: Awaited< + ReturnType + >['paymentManager']; + accountAddress: `0x${string}`; + minimumBalance: number; +}) => { + const { availableBalance } = await paymentManager.getBalance({ + userAddress: accountAddress, + }); + + const currentAvailable = Number(availableBalance); + + if (currentAvailable >= minimumBalance) { + return currentAvailable; + } + + const diff = minimumBalance - currentAvailable; + + console.log( + `🚨 Live Master Account Ledger Balance (${currentAvailable}) is below threshold (${minimumBalance}). Depositing ${difference} ETH.` + ); + + await paymentManager.deposit({ + amountInEth: diff.toString(), + }); + + const { availableBalance: postTopUpBalance } = + await paymentManager.getBalance({ + userAddress: accountAddress, + }); + + console.log('✅ New Live Master Account Payment Balance:', postTopUpBalance); + + return Number(postTopUpBalance); +}; (async () => { // -- Start @@ -43,32 +94,11 @@ const LEDGER_MINIMUM_BALANCE = 10000; ); } - if (LEDGER_MINIMUM_BALANCE > Number(masterAccountDetails.ledgerBalance)) { - // find the difference between the minimum balance and the current balance - const difference = - LEDGER_MINIMUM_BALANCE - Number(masterAccountDetails.ledgerBalance); - - console.log( - `🚨 Live Master Account Ledger Balance is less than LEDGER_MINIMUM_BALANCE: ${LEDGER_MINIMUM_BALANCE} ETH. Attempting to top up the difference of ${difference} ETH to the master account.` - ); - - // deposit the difference - console.log( - '\x1b[90m✅ Depositing the difference to Live Master Account Payment Manager...\x1b[0m' - ); - await masterAccountDetails.paymentManager.deposit({ - amountInEth: difference.toString(), - }); - - // print the new balance - const newBalance = await masterAccountDetails.paymentManager.getBalance({ - userAddress: masterAccount.address, - }); - console.log( - '✅ New Live Master Account Payment Balance:', - newBalance.availableBalance - ); - } + await ensureLedgerThreshold({ + paymentManager: masterAccountDetails.paymentManager, + accountAddress: masterAccount.address, + minimumBalance: LEDGER_MINIMUM_BALANCE, + }); // 3. Authenticate the master account and store the auth data const masterAccountAuthData = await StateManager.getOrUpdate( @@ -145,5 +175,38 @@ const LEDGER_MINIMUM_BALANCE = 10000; // console.log('✅ PKP Sign Test Result:', res); - process.exit(); + if (AUTO_TOP_UP_ENABLED) { + console.log( + `\n✅ Auto top-up enabled. Monitoring every ${AUTO_TOP_UP_INTERVAL}ms with threshold ${AUTO_TOP_UP_THRESHOLD} ETH. Press Ctrl+C to exit.` + ); + + let isTopUpInProgress = false; + + const poll = async () => { + if (isTopUpInProgress) { + return; + } + + isTopUpInProgress = true; + + try { + await ensureLedgerThreshold({ + paymentManager: masterAccountDetails.paymentManager, + accountAddress: masterAccount.address, + minimumBalance: AUTO_TOP_UP_THRESHOLD, + }); + } catch (error) { + console.error('❌ Auto top-up check failed:', error); + } finally { + isTopUpInProgress = false; + } + }; + + await poll(); + setInterval(() => { + void poll(); + }, AUTO_TOP_UP_INTERVAL); + } else { + process.exit(); + } })(); From 9c80c975defb88433bcd101d474dce376e54d11a Mon Sep 17 00:00:00 2001 From: anson Date: Wed, 8 Oct 2025 17:31:44 +0100 Subject: [PATCH 2/9] fix(artillery, init): refactor ledger balance management and add PKP balance checks --- e2e/artillery/src/init.ts | 144 +++++++++++++++++++++----------------- 1 file changed, 81 insertions(+), 63 deletions(-) diff --git a/e2e/artillery/src/init.ts b/e2e/artillery/src/init.ts index 61bc53b5e..2ce83c1d7 100644 --- a/e2e/artillery/src/init.ts +++ b/e2e/artillery/src/init.ts @@ -6,63 +6,54 @@ import { import { createLitClient } from '@lit-protocol/lit-client'; import * as NetworkManager from '../../../e2e/src/helper/NetworkManager'; import { getOrCreatePkp } from '../../../e2e/src/helper/pkp-utils'; +import { printAligned } from '../../../e2e/src/helper/utils'; import '../../src/helper/supressLogs'; import * as AccountManager from '../src/AccountManager'; import * as StateManager from './StateManager'; const _network = process.env['NETWORK']; -const AUTO_TOP_UP_FLAG = '--auto-topup'; -const args = process.argv.slice(2); - // CONFIGURATIONS const REJECT_BALANCE_THRESHOLD = 0; const LEDGER_MINIMUM_BALANCE = 10000; -const AUTO_TOP_UP_ENABLED = args.includes(AUTO_TOP_UP_FLAG); -const AUTO_TOP_UP_INTERVAL = 10_000; -const AUTO_TOP_UP_THRESHOLD = LEDGER_MINIMUM_BALANCE; if (Number.isNaN(LEDGER_MINIMUM_BALANCE) || LEDGER_MINIMUM_BALANCE < 0) { throw new Error('❌ LEDGER_MINIMUM_BALANCE must be a non-negative number'); } -const ensureLedgerThreshold = async ({ - paymentManager, - accountAddress, +const ensureLedgerBalance = async ({ + label, + balanceFetcher, minimumBalance, + topUp, }: { - paymentManager: Awaited< - ReturnType - >['paymentManager']; - accountAddress: `0x${string}`; + label: string; + balanceFetcher: () => Promise<{ availableBalance: string }>; minimumBalance: number; + topUp: (difference: number) => Promise; }) => { - const { availableBalance } = await paymentManager.getBalance({ - userAddress: accountAddress, - }); + const { availableBalance } = await balanceFetcher(); const currentAvailable = Number(availableBalance); if (currentAvailable >= minimumBalance) { + console.log( + `✅ ${label} ledger balance healthy (${currentAvailable} ETH, threshold ${minimumBalance} ETH)` + ); return currentAvailable; } - const diff = minimumBalance - currentAvailable; + const difference = minimumBalance - currentAvailable; console.log( - `🚨 Live Master Account Ledger Balance (${currentAvailable}) is below threshold (${minimumBalance}). Depositing ${difference} ETH.` + `🚨 ${label} ledger balance (${currentAvailable} ETH) is below threshold (${minimumBalance} ETH). Depositing ${difference} ETH.` ); - await paymentManager.deposit({ - amountInEth: diff.toString(), - }); + await topUp(difference); - const { availableBalance: postTopUpBalance } = - await paymentManager.getBalance({ - userAddress: accountAddress, - }); + const { availableBalance: postTopUpBalance } = await balanceFetcher(); - console.log('✅ New Live Master Account Payment Balance:', postTopUpBalance); + console.log(`✅ ${label} ledger balance after top-up: ${postTopUpBalance} ETH`); return Number(postTopUpBalance); }; @@ -94,10 +85,18 @@ const ensureLedgerThreshold = async ({ ); } - await ensureLedgerThreshold({ - paymentManager: masterAccountDetails.paymentManager, - accountAddress: masterAccount.address, + await ensureLedgerBalance({ + label: 'Master Account', + balanceFetcher: () => + masterAccountDetails.paymentManager.getBalance({ + userAddress: masterAccount.address, + }), minimumBalance: LEDGER_MINIMUM_BALANCE, + topUp: async (difference) => { + await masterAccountDetails.paymentManager.deposit({ + amountInEth: difference.toString(), + }); + }, }); // 3. Authenticate the master account and store the auth data @@ -130,6 +129,58 @@ const ensureLedgerThreshold = async ({ console.log('✅ Master Account PKP:', masterAccountPkp); + const pkpEthAddress = masterAccountPkp?.ethAddress; + + if (!pkpEthAddress) { + throw new Error('❌ Master Account PKP is missing an ethAddress'); + } + + const pkpLedgerBalance = await masterAccountDetails.paymentManager.getBalance( + { + userAddress: pkpEthAddress, + } + ); + + console.log('\n========== Master Account PKP Details =========='); + + const pkpStatus = + Number(pkpLedgerBalance.availableBalance) < 0 + ? { + label: '🚨 Status:', + value: `Negative balance (debt): ${pkpLedgerBalance.availableBalance}`, + } + : { label: '', value: '' }; + + printAligned( + [ + { label: '🔑 PKP ETH Address:', value: pkpEthAddress }, + { + label: '💳 Ledger Total Balance:', + value: pkpLedgerBalance.totalBalance, + }, + { + label: '💳 Ledger Available Balance:', + value: pkpLedgerBalance.availableBalance, + }, + pkpStatus, + ].filter((item) => item.label) + ); + + await ensureLedgerBalance({ + label: 'Master Account PKP', + balanceFetcher: () => + masterAccountDetails.paymentManager.getBalance({ + userAddress: pkpEthAddress, + }), + minimumBalance: LEDGER_MINIMUM_BALANCE, + topUp: async (difference) => { + await masterAccountDetails.paymentManager.depositForUser({ + userAddress: pkpEthAddress, + amountInEth: difference.toString(), + }); + }, + }); + // create pkp auth context // const masterAccountPkpAuthContext = await authManager.createPkpAuthContext({ // authData: masterAccountAuthData, @@ -175,38 +226,5 @@ const ensureLedgerThreshold = async ({ // console.log('✅ PKP Sign Test Result:', res); - if (AUTO_TOP_UP_ENABLED) { - console.log( - `\n✅ Auto top-up enabled. Monitoring every ${AUTO_TOP_UP_INTERVAL}ms with threshold ${AUTO_TOP_UP_THRESHOLD} ETH. Press Ctrl+C to exit.` - ); - - let isTopUpInProgress = false; - - const poll = async () => { - if (isTopUpInProgress) { - return; - } - - isTopUpInProgress = true; - - try { - await ensureLedgerThreshold({ - paymentManager: masterAccountDetails.paymentManager, - accountAddress: masterAccount.address, - minimumBalance: AUTO_TOP_UP_THRESHOLD, - }); - } catch (error) { - console.error('❌ Auto top-up check failed:', error); - } finally { - isTopUpInProgress = false; - } - }; - - await poll(); - setInterval(() => { - void poll(); - }, AUTO_TOP_UP_INTERVAL); - } else { - process.exit(); - } + process.exit(); })(); From 615614088fbf1503598ddd694590b074bf1c3933 Mon Sep 17 00:00:00 2001 From: anson Date: Wed, 8 Oct 2025 17:37:02 +0100 Subject: [PATCH 3/9] fix(init): update ledger minimum balance constants and validation checks --- e2e/artillery/src/init.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/e2e/artillery/src/init.ts b/e2e/artillery/src/init.ts index 2ce83c1d7..f5fb1f217 100644 --- a/e2e/artillery/src/init.ts +++ b/e2e/artillery/src/init.ts @@ -15,10 +15,13 @@ const _network = process.env['NETWORK']; // CONFIGURATIONS const REJECT_BALANCE_THRESHOLD = 0; -const LEDGER_MINIMUM_BALANCE = 10000; +const MASTER_LEDGER_MINIMUM_BALANCE = 10_000; +const PKP_LEDGER_MINIMUM_BALANCE = 10_000; -if (Number.isNaN(LEDGER_MINIMUM_BALANCE) || LEDGER_MINIMUM_BALANCE < 0) { - throw new Error('❌ LEDGER_MINIMUM_BALANCE must be a non-negative number'); +if (MASTER_LEDGER_MINIMUM_BALANCE < 0 || PKP_LEDGER_MINIMUM_BALANCE < 0) { + throw new Error( + '❌ Ledger minimum balances must be non-negative numbers' + ); } const ensureLedgerBalance = async ({ @@ -91,7 +94,7 @@ const ensureLedgerBalance = async ({ masterAccountDetails.paymentManager.getBalance({ userAddress: masterAccount.address, }), - minimumBalance: LEDGER_MINIMUM_BALANCE, + minimumBalance: MASTER_LEDGER_MINIMUM_BALANCE, topUp: async (difference) => { await masterAccountDetails.paymentManager.deposit({ amountInEth: difference.toString(), @@ -172,7 +175,7 @@ const ensureLedgerBalance = async ({ masterAccountDetails.paymentManager.getBalance({ userAddress: pkpEthAddress, }), - minimumBalance: LEDGER_MINIMUM_BALANCE, + minimumBalance: PKP_LEDGER_MINIMUM_BALANCE, topUp: async (difference) => { await masterAccountDetails.paymentManager.depositForUser({ userAddress: pkpEthAddress, From e15070aa4de4645eca7a7e5aee8bdf2ac92d6496 Mon Sep 17 00:00:00 2001 From: anson Date: Wed, 8 Oct 2025 17:37:12 +0100 Subject: [PATCH 4/9] fix(init): update master and PKP ledger minimum balance constants --- e2e/artillery/src/init.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/artillery/src/init.ts b/e2e/artillery/src/init.ts index f5fb1f217..29392d860 100644 --- a/e2e/artillery/src/init.ts +++ b/e2e/artillery/src/init.ts @@ -15,8 +15,8 @@ const _network = process.env['NETWORK']; // CONFIGURATIONS const REJECT_BALANCE_THRESHOLD = 0; -const MASTER_LEDGER_MINIMUM_BALANCE = 10_000; -const PKP_LEDGER_MINIMUM_BALANCE = 10_000; +const MASTER_LEDGER_MINIMUM_BALANCE = 3_000; +const PKP_LEDGER_MINIMUM_BALANCE = 3_000; if (MASTER_LEDGER_MINIMUM_BALANCE < 0 || PKP_LEDGER_MINIMUM_BALANCE < 0) { throw new Error( From e5c1c460c9ef2f26e65d7cc312226b7ada606d2a Mon Sep 17 00:00:00 2001 From: anson Date: Wed, 8 Oct 2025 17:39:07 +0100 Subject: [PATCH 5/9] fmt --- e2e/artillery/src/init.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e/artillery/src/init.ts b/e2e/artillery/src/init.ts index 29392d860..f1c96cae3 100644 --- a/e2e/artillery/src/init.ts +++ b/e2e/artillery/src/init.ts @@ -19,9 +19,7 @@ const MASTER_LEDGER_MINIMUM_BALANCE = 3_000; const PKP_LEDGER_MINIMUM_BALANCE = 3_000; if (MASTER_LEDGER_MINIMUM_BALANCE < 0 || PKP_LEDGER_MINIMUM_BALANCE < 0) { - throw new Error( - '❌ Ledger minimum balances must be non-negative numbers' - ); + throw new Error('❌ Ledger minimum balances must be non-negative numbers'); } const ensureLedgerBalance = async ({ @@ -56,7 +54,9 @@ const ensureLedgerBalance = async ({ const { availableBalance: postTopUpBalance } = await balanceFetcher(); - console.log(`✅ ${label} ledger balance after top-up: ${postTopUpBalance} ETH`); + console.log( + `✅ ${label} ledger balance after top-up: ${postTopUpBalance} ETH` + ); return Number(postTopUpBalance); }; From 35da202d8b81c485e008daa9f58aa40eb6575183 Mon Sep 17 00:00:00 2001 From: anson Date: Wed, 8 Oct 2025 17:40:59 +0100 Subject: [PATCH 6/9] fmt --- e2e/artillery/src/init.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e/artillery/src/init.ts b/e2e/artillery/src/init.ts index f1c96cae3..ff5414694 100644 --- a/e2e/artillery/src/init.ts +++ b/e2e/artillery/src/init.ts @@ -1,15 +1,15 @@ +import '../../src/helper/supressLogs'; import { createAuthManager, storagePlugins, ViemAccountAuthenticator, } from '@lit-protocol/auth'; +import * as StateManager from './StateManager'; import { createLitClient } from '@lit-protocol/lit-client'; -import * as NetworkManager from '../../../e2e/src/helper/NetworkManager'; import { getOrCreatePkp } from '../../../e2e/src/helper/pkp-utils'; -import { printAligned } from '../../../e2e/src/helper/utils'; -import '../../src/helper/supressLogs'; +import * as NetworkManager from '../../../e2e/src/helper/NetworkManager'; import * as AccountManager from '../src/AccountManager'; -import * as StateManager from './StateManager'; +import { printAligned } from '../../../e2e/src/helper/utils'; const _network = process.env['NETWORK']; From 208ec2cb728adb07afbdaed78b255f466db85d16 Mon Sep 17 00:00:00 2001 From: anson Date: Wed, 8 Oct 2025 15:24:00 +0100 Subject: [PATCH 7/9] feat(pricing): add `sign-session-key` pricing support and product-aware sorting - request `SIGN_SESSION_KEY` prices when fetching node data so the contract returns all four product columns - extend pricing context schema to accept `SIGN_SESSION_KEY` and re-sort nodes per product before slicing to threshold - route PKP auth through `SIGN_SESSION_KEY` pricing while keeping custom auth on `LIT_ACTION` - cover the new sort logic with a unit test to prove we now pick the cheapest validators for the requested product --- .../authAdapters/getPkpAuthContextAdapter.ts | 4 +- .../pricing/getNodesForRequest.ts | 1 + .../getMaxPricesForNodeProduct.spec.ts | 60 +++++++++++++++++++ .../getMaxPricesForNodeProduct.ts | 18 +++++- .../shared/managers/pricing-manager/schema.ts | 2 +- 5 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.spec.ts diff --git a/packages/auth/src/lib/AuthManager/authAdapters/getPkpAuthContextAdapter.ts b/packages/auth/src/lib/AuthManager/authAdapters/getPkpAuthContextAdapter.ts index eb39b1c11..36eefad0a 100644 --- a/packages/auth/src/lib/AuthManager/authAdapters/getPkpAuthContextAdapter.ts +++ b/packages/auth/src/lib/AuthManager/authAdapters/getPkpAuthContextAdapter.ts @@ -136,9 +136,9 @@ export async function getPkpAuthContextAdapter( const nodeUrls = litClientCtx.getMaxPricesForNodeProduct({ nodePrices: nodePrices, userMaxPrice: litClientCtx.getUserMaxPrice({ - product: 'LIT_ACTION', + product: 'SIGN_SESSION_KEY', }), - productId: PRODUCT_IDS['LIT_ACTION'], + productId: PRODUCT_IDS['SIGN_SESSION_KEY'], numRequiredNodes: threshold, }); diff --git a/packages/networks/src/networks/vNaga/shared/managers/LitChainClient/apis/rawContractApis/pricing/getNodesForRequest.ts b/packages/networks/src/networks/vNaga/shared/managers/LitChainClient/apis/rawContractApis/pricing/getNodesForRequest.ts index e4a28fbec..be3f7aa13 100644 --- a/packages/networks/src/networks/vNaga/shared/managers/LitChainClient/apis/rawContractApis/pricing/getNodesForRequest.ts +++ b/packages/networks/src/networks/vNaga/shared/managers/LitChainClient/apis/rawContractApis/pricing/getNodesForRequest.ts @@ -15,6 +15,7 @@ import { * - DECRYPTION (0): Used for decryption operations * - SIGN (1): Used for signing operations * - LA (2): Used for Lit Actions execution + * - SIGN_SESSION_KEY (3): Used for sign session key operations */ export const PRODUCT_IDS = { DECRYPTION: 0n, // For decryption operations diff --git a/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.spec.ts b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.spec.ts new file mode 100644 index 000000000..2dcef396c --- /dev/null +++ b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.spec.ts @@ -0,0 +1,60 @@ +import { PRODUCT_IDS } from '@lit-protocol/constants'; + +import { getMaxPricesForNodeProduct } from './getMaxPricesForNodeProduct'; + +describe('getMaxPricesForNodeProduct', () => { + it('uses the requested product column when ranking nodes', () => { + const nodePrices = [ + { + url: 'https://node-a', + prices: [80n, 5n, 9n, 30n], + }, + { + url: 'https://node-b', + prices: [70n, 4n, 8n, 10n], + }, + { + url: 'https://node-c', + prices: [60n, 3n, 7n, 20n], + }, + ]; + + // Log the incoming order to show the encryption column is already sorted lowest-first. + console.log( + 'incoming order', + nodePrices.map(({ url, prices }) => ({ + url, + decryptionPrice: prices[PRODUCT_IDS.DECRYPTION], + signPrice: prices[PRODUCT_IDS.SIGN], + litActionPrice: prices[PRODUCT_IDS.LIT_ACTION], + signSessionKeyPrice: prices[PRODUCT_IDS.SIGN_SESSION_KEY], + })) + ); + + // Call the helper exactly like the SDK does: ask for SIGN_SESSION_KEY prices, + // pass the raw price feed output, and cap the request at two nodes. + const result = getMaxPricesForNodeProduct({ + nodePrices, + userMaxPrice: 100n, + productId: PRODUCT_IDS.SIGN_SESSION_KEY, + numRequiredNodes: 2, + }); + + console.log( + 'selected nodes', + result.map(({ url, price }) => ({ url, price })) + ); + + // After sorting the nodes by the session-key column, the helper should + // return node-b (10) and node-c (20) even though the original array was + // ordered by the decryption price column. + expect(result).toHaveLength(2); + expect(result[0].url).toBe('https://node-b'); + expect(result[1].url).toBe('https://node-c'); + + // Base prices are taken from the SIGN_SESSION_KEY column (10 and 20) + // with the excess (100 - 30 = 70) split evenly. + expect(result[0].price).toBe(10n + 35n); + expect(result[1].price).toBe(20n + 35n); + }); +}); diff --git a/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.ts b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.ts index 5d90c4ed4..461b0c2d2 100644 --- a/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.ts +++ b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.ts @@ -28,10 +28,24 @@ export function getMaxPricesForNodeProduct({ productId, numRequiredNodes, }: MaxPricesForNodes): { url: string; price: bigint }[] { + // Always evaluate pricing using the product-specific column so we truly pick + // the cheapest validators for that product (the upstream feed is sorted by + // prices[0]/decryption price only). + const sortedNodes = [...nodePrices].sort((a, b) => { + const priceA = a.prices[productId]; + const priceB = b.prices[productId]; + + if (priceA === priceB) { + return 0; + } + + return priceA < priceB ? -1 : 1; + }); + // If we don't need all nodes to service the request, only use the cheapest `n` of them const nodesToConsider = numRequiredNodes - ? nodePrices.slice(0, numRequiredNodes) - : nodePrices; + ? sortedNodes.slice(0, numRequiredNodes) + : sortedNodes; let totalBaseCost = 0n; diff --git a/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/schema.ts b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/schema.ts index 4a62484ad..7f99c6b8a 100644 --- a/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/schema.ts +++ b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/schema.ts @@ -4,7 +4,7 @@ import { PRODUCT_IDS } from './constants'; export const PricingContextSchema = z .object({ - product: z.enum(['DECRYPTION', 'SIGN', 'LIT_ACTION']), + product: z.enum(['DECRYPTION', 'SIGN', 'LIT_ACTION', 'SIGN_SESSION_KEY']), userMaxPrice: z.bigint().optional(), nodePrices: z.array( z.object({ url: z.string(), prices: z.array(z.bigint()) }) From a78ac1b28a9eac88c408482276775e38848c5886 Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 8 Oct 2025 15:37:15 +0100 Subject: [PATCH 8/9] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Anson --- .../managers/pricing-manager/getMaxPricesForNodeProduct.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.spec.ts b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.spec.ts index 2dcef396c..85cbc5493 100644 --- a/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.spec.ts +++ b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.spec.ts @@ -19,7 +19,7 @@ describe('getMaxPricesForNodeProduct', () => { }, ]; - // Log the incoming order to show the encryption column is already sorted lowest-first. + // Log the incoming order to show the decryption column is already sorted lowest-first. console.log( 'incoming order', nodePrices.map(({ url, prices }) => ({ From e4bac2fc8f7666bfa731bcc97312da5f812b6873 Mon Sep 17 00:00:00 2001 From: anson Date: Wed, 8 Oct 2025 17:11:52 +0100 Subject: [PATCH 9/9] docs(pricing): improve comments --- .../getMaxPricesForNodeProduct.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.ts b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.ts index 461b0c2d2..3768ba3cd 100644 --- a/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.ts +++ b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.ts @@ -15,6 +15,13 @@ export interface MaxPricesForNodes { * Ensures the total cost does not exceed userMaxPrice. * Operates in the order of lowest priced node to highest. * + * Example: + * - Selected nodes have SIGN_SESSION_KEY prices of 10 and 20. + * - `userMaxPrice` is 100. + * - Base total = 10 + 20 = 30. + * - Excess = 100 - 30 = 70. + * - Each node receives 70 / 2 = 35 extra budget, yielding 45 and 55. + * * @param nodePrices - An object where keys are node addresses and values are arrays of prices for different action types. * @param userMaxPrice - The maximum price the user is willing to pay to execute the request. * @param productId - The ID of the product to determine which price to consider. @@ -47,14 +54,14 @@ export function getMaxPricesForNodeProduct({ ? sortedNodes.slice(0, numRequiredNodes) : sortedNodes; + // Sum the unadjusted cost for the nodes we plan to use. let totalBaseCost = 0n; - - // Calculate the base total cost without adjustments for (const { prices } of nodesToConsider) { + // Example: base total accumulates 10 + 20 = 30 for the two cheapest nodes. totalBaseCost += prices[productId]; } - // Verify that we have a high enough userMaxPrice to fulfill the request + // Refuse to proceed if the caller's budget cannot even cover the base cost. if (totalBaseCost > userMaxPrice) { throw new MaxPriceTooLow( { @@ -72,13 +79,16 @@ export function getMaxPricesForNodeProduct({ * our request to fail if the price on some of the nodes is higher than we think it was, as long as it's not * drastically different than we expect it to be */ + // Any remaining budget is spread across the participating nodes to + // provide cushion for minor pricing fluctuations. Example: 100 - 30 = 70. const excessBalance = userMaxPrice - totalBaseCost; // Map matching the keys from `nodePrices`, but w/ the per-node maxPrice computed based on `userMaxPrice` const maxPricesPerNode: { url: string; price: bigint }[] = []; for (const { url, prices } of nodesToConsider) { - // For now, we'll distribute the remaining balance equally across nodes + // Distribute the remaining budget evenly across nodes to form the max price. + // Example: each node receives 70 / 2 = 35, becoming 10+35 and 20+35. maxPricesPerNode.push({ url, price: excessBalance