mppx 0.6.17 → 0.6.18
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/CHANGELOG.md +8 -0
- package/dist/cli/cli.d.ts +4 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +230 -10
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/plugins/plugin.d.ts +5 -0
- package/dist/cli/plugins/plugin.d.ts.map +1 -1
- package/dist/cli/plugins/plugin.js.map +1 -1
- package/dist/cli/plugins/stripe.d.ts.map +1 -1
- package/dist/cli/plugins/stripe.js +7 -2
- package/dist/cli/plugins/stripe.js.map +1 -1
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +69 -9
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/cli/utils.d.ts +10 -2
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js +11 -4
- package/dist/cli/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/cli.test.ts +120 -0
- package/src/cli/cli.ts +254 -10
- package/src/cli/mcp.test.ts +3 -0
- package/src/cli/plugins/plugin.ts +9 -1
- package/src/cli/plugins/stripe.ts +7 -0
- package/src/cli/plugins/tempo.ts +83 -12
- package/src/cli/utils.test.ts +12 -1
- package/src/cli/utils.ts +23 -5
package/src/cli/mcp.test.ts
CHANGED
|
@@ -176,6 +176,9 @@ test('tools/list exposes mppx commands with input and output schemas', async ()
|
|
|
176
176
|
'discover_generate',
|
|
177
177
|
'discover_validate',
|
|
178
178
|
'init',
|
|
179
|
+
'services_endpoints',
|
|
180
|
+
'services_list',
|
|
181
|
+
'services_show',
|
|
179
182
|
'sign',
|
|
180
183
|
])
|
|
181
184
|
expect(tools.find((tool: { name: string }) => tool.name === 'account_list').outputSchema).toEqual(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type * as Challenge from '../../Challenge.js'
|
|
2
2
|
import type * as Method from '../../Method.js'
|
|
3
|
+
import type { Network } from '../utils.js'
|
|
3
4
|
|
|
4
5
|
export function createPlugin(plugin: Plugin): Plugin {
|
|
5
6
|
return plugin
|
|
@@ -18,7 +19,14 @@ export interface Plugin {
|
|
|
18
19
|
*/
|
|
19
20
|
setup(ctx: {
|
|
20
21
|
challenge: Challenge.Challenge
|
|
21
|
-
options: {
|
|
22
|
+
options: {
|
|
23
|
+
account?: string | undefined
|
|
24
|
+
autoSwap?: boolean | undefined
|
|
25
|
+
network?: Network | undefined
|
|
26
|
+
payWith?: string | undefined
|
|
27
|
+
rpcUrl?: string | undefined
|
|
28
|
+
slippage?: number | undefined
|
|
29
|
+
}
|
|
22
30
|
methodOpts: Record<string, string>
|
|
23
31
|
}): Promise<{
|
|
24
32
|
/** Token symbol for display (e.g., 'PathUSD', 'USD') */
|
|
@@ -20,6 +20,7 @@ export function stripe() {
|
|
|
20
20
|
paymentMethod: z.string(),
|
|
21
21
|
}),
|
|
22
22
|
methodOpts,
|
|
23
|
+
['paymentMethod'],
|
|
23
24
|
)
|
|
24
25
|
|
|
25
26
|
const stripeSecretKey = process.env.MPPX_STRIPE_SECRET_KEY
|
|
@@ -134,7 +135,13 @@ export function stripe() {
|
|
|
134
135
|
function parseOptions<const schema extends z.ZodType>(
|
|
135
136
|
schema: schema,
|
|
136
137
|
rawOptions: unknown,
|
|
138
|
+
allowedKeys: readonly string[],
|
|
137
139
|
): z.output<schema> {
|
|
140
|
+
if (rawOptions && typeof rawOptions === 'object' && !Array.isArray(rawOptions)) {
|
|
141
|
+
const unknownKeys = Object.keys(rawOptions).filter((key) => !allowedKeys.includes(key))
|
|
142
|
+
if (unknownKeys.length)
|
|
143
|
+
throw new Error(`Unsupported CLI method option(s): ${unknownKeys.join(', ')}`)
|
|
144
|
+
}
|
|
138
145
|
const result = schema.safeParse(rawOptions ?? {})
|
|
139
146
|
if (result.success) return result.data
|
|
140
147
|
const summary = result.error.issues
|
package/src/cli/plugins/tempo.ts
CHANGED
|
@@ -54,6 +54,29 @@ export function tempo() {
|
|
|
54
54
|
const accountName = resolveAccountName(options.account)
|
|
55
55
|
const challengeRequest = challenge.request as Record<string, unknown>
|
|
56
56
|
const currency = challengeRequest.currency as string | undefined
|
|
57
|
+
const booleanOption = z.union([
|
|
58
|
+
z.boolean(),
|
|
59
|
+
z.literal('true').transform(() => true),
|
|
60
|
+
z.literal('false').transform(() => false),
|
|
61
|
+
])
|
|
62
|
+
const tempoOpts = parseOptions(
|
|
63
|
+
z.object({
|
|
64
|
+
autoSwap: z.optional(booleanOption),
|
|
65
|
+
channel: z.optional(z.coerce.string()),
|
|
66
|
+
deposit: z.optional(z.union([z.string(), z.number()])),
|
|
67
|
+
payWith: z.optional(z.string()),
|
|
68
|
+
slippage: z.optional(z.coerce.number()),
|
|
69
|
+
tokenIn: z.optional(z.string()),
|
|
70
|
+
}),
|
|
71
|
+
methodOpts,
|
|
72
|
+
['autoSwap', 'channel', 'deposit', 'payWith', 'slippage', 'tokenIn'],
|
|
73
|
+
)
|
|
74
|
+
const autoSwap = resolveAutoSwap({
|
|
75
|
+
autoSwap: tempoOpts.autoSwap ?? options.autoSwap,
|
|
76
|
+
payWith: tempoOpts.payWith ?? options.payWith,
|
|
77
|
+
slippage: tempoOpts.slippage ?? options.slippage,
|
|
78
|
+
tokenIn: tempoOpts.tokenIn,
|
|
79
|
+
})
|
|
57
80
|
|
|
58
81
|
let tokenSymbol = currency ?? ''
|
|
59
82
|
let tokenDecimals = (challengeRequest.decimals as number | undefined) ?? 6
|
|
@@ -71,11 +94,12 @@ export function tempo() {
|
|
|
71
94
|
useTempoCliSign = true
|
|
72
95
|
const tempoEntry = resolveTempoAccount(accountName)
|
|
73
96
|
if (tempoEntry) {
|
|
74
|
-
const rpcUrl = resolveRpcUrl(options.rpcUrl)
|
|
97
|
+
const rpcUrl = resolveRpcUrl(options.rpcUrl, { network: options.network })
|
|
75
98
|
client = createClient({
|
|
76
|
-
chain: await resolveChain({ rpcUrl }),
|
|
99
|
+
chain: await resolveChain({ network: options.network, rpcUrl }),
|
|
77
100
|
transport: http(rpcUrl),
|
|
78
101
|
})
|
|
102
|
+
assertChallengeChain({ challenge, clientChainId: client.chain?.id })
|
|
79
103
|
explorerUrl = client.chain?.blockExplorers?.default?.url
|
|
80
104
|
const tokenInfo = currency
|
|
81
105
|
? await fetchTokenInfo(
|
|
@@ -111,11 +135,12 @@ export function tempo() {
|
|
|
111
135
|
} else account = privateKeyToAccount(privateKey as `0x${string}`)
|
|
112
136
|
|
|
113
137
|
if (!useTempoCliSign && account) {
|
|
114
|
-
const rpcUrl = resolveRpcUrl(options.rpcUrl)
|
|
138
|
+
const rpcUrl = resolveRpcUrl(options.rpcUrl, { network: options.network })
|
|
115
139
|
client = createClient({
|
|
116
|
-
chain: await resolveChain({ rpcUrl }),
|
|
140
|
+
chain: await resolveChain({ network: options.network, rpcUrl }),
|
|
117
141
|
transport: http(rpcUrl),
|
|
118
142
|
})
|
|
143
|
+
assertChallengeChain({ challenge, clientChainId: client.chain?.id })
|
|
119
144
|
explorerUrl = client.chain?.blockExplorers?.default?.url
|
|
120
145
|
const tokenInfo = currency
|
|
121
146
|
? await fetchTokenInfo(client, currency as Address, account.address).catch(
|
|
@@ -147,17 +172,10 @@ export function tempo() {
|
|
|
147
172
|
exitCode: 69,
|
|
148
173
|
})
|
|
149
174
|
|
|
150
|
-
const tempoOpts = parseOptions(
|
|
151
|
-
z.object({
|
|
152
|
-
channel: z.optional(z.coerce.string()),
|
|
153
|
-
deposit: z.optional(z.union([z.string(), z.number()])),
|
|
154
|
-
}),
|
|
155
|
-
methodOpts,
|
|
156
|
-
)
|
|
157
|
-
|
|
158
175
|
const methods = tempoMethods({
|
|
159
176
|
account,
|
|
160
177
|
getClient: () => client!,
|
|
178
|
+
...(autoSwap !== undefined ? { autoSwap } : {}),
|
|
161
179
|
deposit: (() => {
|
|
162
180
|
if (challenge.intent !== 'session') return undefined
|
|
163
181
|
const suggestedDeposit = (challenge.request as Record<string, unknown>)
|
|
@@ -718,7 +736,13 @@ function detectTerminalBg(
|
|
|
718
736
|
function parseOptions<const schema extends z.ZodType>(
|
|
719
737
|
schema: schema,
|
|
720
738
|
rawOptions: unknown,
|
|
739
|
+
allowedKeys: readonly string[],
|
|
721
740
|
): z.output<schema> {
|
|
741
|
+
if (rawOptions && typeof rawOptions === 'object' && !Array.isArray(rawOptions)) {
|
|
742
|
+
const unknownKeys = Object.keys(rawOptions).filter((key) => !allowedKeys.includes(key))
|
|
743
|
+
if (unknownKeys.length)
|
|
744
|
+
throw new Error(`Unsupported CLI method option(s): ${unknownKeys.join(', ')}`)
|
|
745
|
+
}
|
|
722
746
|
const result = schema.safeParse(rawOptions ?? {})
|
|
723
747
|
if (result.success) return result.data
|
|
724
748
|
const summary = result.error.issues
|
|
@@ -730,6 +754,53 @@ function parseOptions<const schema extends z.ZodType>(
|
|
|
730
754
|
throw new Error(`Invalid CLI options (${summary})`)
|
|
731
755
|
}
|
|
732
756
|
|
|
757
|
+
function assertChallengeChain(opts: {
|
|
758
|
+
challenge: { request: Record<string, unknown> }
|
|
759
|
+
clientChainId?: number | undefined
|
|
760
|
+
}) {
|
|
761
|
+
const methodDetails = opts.challenge.request.methodDetails as
|
|
762
|
+
| { chainId?: number | undefined }
|
|
763
|
+
| undefined
|
|
764
|
+
const requiredChainId = methodDetails?.chainId
|
|
765
|
+
if (!requiredChainId || !opts.clientChainId || requiredChainId === opts.clientChainId) return
|
|
766
|
+
const hint =
|
|
767
|
+
requiredChainId === 4217
|
|
768
|
+
? ' Use --network mainnet or --rpc-url https://rpc.tempo.xyz.'
|
|
769
|
+
: requiredChainId === 42431
|
|
770
|
+
? ' Use --network testnet or --rpc-url https://rpc.moderato.tempo.xyz.'
|
|
771
|
+
: ''
|
|
772
|
+
throw new Errors.IncurError({
|
|
773
|
+
code: 'CHAIN_MISMATCH',
|
|
774
|
+
message: `Challenge requires chainId ${requiredChainId}, but RPC is chainId ${opts.clientChainId}.${hint}`,
|
|
775
|
+
exitCode: 2,
|
|
776
|
+
})
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function parseTokenList(value: string | undefined): Address[] | undefined {
|
|
780
|
+
if (!value) return undefined
|
|
781
|
+
return value
|
|
782
|
+
.split(',')
|
|
783
|
+
.map((token) => token.trim())
|
|
784
|
+
.filter(Boolean) as Address[]
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function resolveAutoSwap(opts: {
|
|
788
|
+
autoSwap?: boolean | undefined
|
|
789
|
+
payWith?: string | undefined
|
|
790
|
+
slippage?: number | undefined
|
|
791
|
+
tokenIn?: string | undefined
|
|
792
|
+
}) {
|
|
793
|
+
const tokenIn = parseTokenList(opts.tokenIn) ?? parseTokenList(opts.payWith)
|
|
794
|
+
if (!opts.autoSwap && !tokenIn && opts.slippage === undefined) return undefined
|
|
795
|
+
if (opts.autoSwap === false && !tokenIn && opts.slippage === undefined) return false
|
|
796
|
+
if (opts.slippage !== undefined && (!Number.isFinite(opts.slippage) || opts.slippage < 0))
|
|
797
|
+
throw new Error('Invalid CLI options (slippage: expected a non-negative number)')
|
|
798
|
+
return {
|
|
799
|
+
...(tokenIn ? { tokenIn } : {}),
|
|
800
|
+
...(opts.slippage !== undefined ? { slippage: opts.slippage } : {}),
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
733
804
|
function channelStateDir() {
|
|
734
805
|
return path.join(
|
|
735
806
|
process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'),
|
package/src/cli/utils.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tempo as tempoMainnet, tempoModerato } from 'viem/chains'
|
|
2
2
|
import { afterEach, describe, expect, test } from 'vp/test'
|
|
3
3
|
|
|
4
|
-
import { resolveChain, resolveRpcUrl } from './utils.js'
|
|
4
|
+
import { networkRpcUrls, resolveChain, resolveRpcUrl } from './utils.js'
|
|
5
5
|
|
|
6
6
|
describe('resolveRpcUrl', () => {
|
|
7
7
|
afterEach(() => {
|
|
@@ -14,6 +14,17 @@ describe('resolveRpcUrl', () => {
|
|
|
14
14
|
expect(resolveRpcUrl('https://explicit.example.com')).toBe('https://explicit.example.com')
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
+
test('uses network default before env vars', () => {
|
|
18
|
+
process.env.MPPX_RPC_URL = 'https://env.example.com'
|
|
19
|
+
expect(resolveRpcUrl(undefined, { network: 'testnet' })).toBe(networkRpcUrls.testnet)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('prefers explicit rpc url over network default', () => {
|
|
23
|
+
expect(resolveRpcUrl('https://explicit.example.com', { network: 'mainnet' })).toBe(
|
|
24
|
+
'https://explicit.example.com',
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
|
|
17
28
|
test('falls back to MPPX_RPC_URL env var', () => {
|
|
18
29
|
process.env.MPPX_RPC_URL = 'https://mppx.example.com'
|
|
19
30
|
process.env.RPC_URL = 'https://rpc.example.com'
|
package/src/cli/utils.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { Chain } from 'viem'
|
|
|
4
4
|
import { type Address, createClient, http } from 'viem'
|
|
5
5
|
import { tempo as tempoMainnet, tempoModerato } from 'viem/chains'
|
|
6
6
|
|
|
7
|
+
import * as defaults from '../tempo/internal/defaults.js'
|
|
8
|
+
|
|
7
9
|
// Inlined from https://github.com/alexeyraspopov/picocolors (ISC License)
|
|
8
10
|
export const pc = (() => {
|
|
9
11
|
const p = process || ({} as NodeJS.Process)
|
|
@@ -221,13 +223,29 @@ export function fmtBalance(
|
|
|
221
223
|
return `${dec ? `${formatted}.${dec}` : formatted} ${sym}`
|
|
222
224
|
}
|
|
223
225
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
export type Network = 'mainnet' | 'testnet'
|
|
227
|
+
|
|
228
|
+
export const networkRpcUrls = {
|
|
229
|
+
mainnet: defaults.rpcUrl[defaults.chainId.mainnet],
|
|
230
|
+
testnet: defaults.rpcUrl[defaults.chainId.testnet],
|
|
231
|
+
} as const satisfies Record<Network, string>
|
|
232
|
+
|
|
233
|
+
/** Resolve RPC URL from explicit option, network option, then MPPX_RPC_URL/RPC_URL env vars. */
|
|
234
|
+
export function resolveRpcUrl(
|
|
235
|
+
explicit?: string | undefined,
|
|
236
|
+
options: { network?: Network | undefined } = {},
|
|
237
|
+
): string | undefined {
|
|
238
|
+
return (
|
|
239
|
+
explicit ??
|
|
240
|
+
(options.network ? networkRpcUrls[options.network] : undefined) ??
|
|
241
|
+
(process.env.MPPX_RPC_URL?.trim() || process.env.RPC_URL?.trim() || undefined)
|
|
242
|
+
)
|
|
227
243
|
}
|
|
228
244
|
|
|
229
|
-
export async function resolveChain(
|
|
230
|
-
|
|
245
|
+
export async function resolveChain(
|
|
246
|
+
opts: { network?: Network | undefined; rpcUrl?: string | undefined } = {},
|
|
247
|
+
): Promise<Chain> {
|
|
248
|
+
const rpcUrl = resolveRpcUrl(opts.rpcUrl, { network: opts.network })
|
|
231
249
|
if (!rpcUrl) return tempoMainnet
|
|
232
250
|
const { getChainId } = await import('viem/actions')
|
|
233
251
|
const chainId = await getChainId(createClient({ transport: http(rpcUrl) }))
|