@manifest-network/manifest-mcp-browser 0.1.1 → 0.1.6

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.
Files changed (131) hide show
  1. package/.github/workflows/publish.yml +4 -2
  2. package/CLAUDE.md +11 -6
  3. package/README.md +1 -1
  4. package/dist/client.d.ts +6 -1
  5. package/dist/client.d.ts.map +1 -1
  6. package/dist/client.js +77 -21
  7. package/dist/client.js.map +1 -1
  8. package/dist/cosmos.d.ts.map +1 -1
  9. package/dist/cosmos.js +7 -57
  10. package/dist/cosmos.js.map +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +16 -25
  13. package/dist/index.js.map +1 -1
  14. package/dist/modules.d.ts +30 -1
  15. package/dist/modules.d.ts.map +1 -1
  16. package/dist/modules.js +98 -1
  17. package/dist/modules.js.map +1 -1
  18. package/dist/modules.test.js +60 -1
  19. package/dist/modules.test.js.map +1 -1
  20. package/dist/queries/auth.d.ts +7 -1
  21. package/dist/queries/auth.d.ts.map +1 -1
  22. package/dist/queries/auth.js +18 -45
  23. package/dist/queries/auth.js.map +1 -1
  24. package/dist/queries/bank.d.ts +7 -1
  25. package/dist/queries/bank.d.ts.map +1 -1
  26. package/dist/queries/bank.js +24 -38
  27. package/dist/queries/bank.js.map +1 -1
  28. package/dist/queries/billing.d.ts +7 -1
  29. package/dist/queries/billing.d.ts.map +1 -1
  30. package/dist/queries/billing.js +28 -54
  31. package/dist/queries/billing.js.map +1 -1
  32. package/dist/queries/distribution.d.ts +7 -1
  33. package/dist/queries/distribution.d.ts.map +1 -1
  34. package/dist/queries/distribution.js +18 -36
  35. package/dist/queries/distribution.js.map +1 -1
  36. package/dist/queries/gov.d.ts +7 -1
  37. package/dist/queries/gov.d.ts.map +1 -1
  38. package/dist/queries/gov.js +24 -41
  39. package/dist/queries/gov.js.map +1 -1
  40. package/dist/queries/index.d.ts +2 -1
  41. package/dist/queries/index.d.ts.map +1 -1
  42. package/dist/queries/index.js +1 -1
  43. package/dist/queries/index.js.map +1 -1
  44. package/dist/queries/sku.d.ts +13 -0
  45. package/dist/queries/sku.d.ts.map +1 -0
  46. package/dist/queries/sku.js +60 -0
  47. package/dist/queries/sku.js.map +1 -0
  48. package/dist/queries/staking.d.ts +7 -1
  49. package/dist/queries/staking.d.ts.map +1 -1
  50. package/dist/queries/staking.js +36 -59
  51. package/dist/queries/staking.js.map +1 -1
  52. package/dist/queries/utils.d.ts +60 -10
  53. package/dist/queries/utils.d.ts.map +1 -1
  54. package/dist/queries/utils.js +80 -12
  55. package/dist/queries/utils.js.map +1 -1
  56. package/dist/queries/utils.test.js +68 -8
  57. package/dist/queries/utils.test.js.map +1 -1
  58. package/dist/transactions/bank.d.ts +2 -2
  59. package/dist/transactions/bank.d.ts.map +1 -1
  60. package/dist/transactions/bank.js +9 -18
  61. package/dist/transactions/bank.js.map +1 -1
  62. package/dist/transactions/billing.d.ts +2 -2
  63. package/dist/transactions/billing.d.ts.map +1 -1
  64. package/dist/transactions/billing.js +125 -89
  65. package/dist/transactions/billing.js.map +1 -1
  66. package/dist/transactions/distribution.d.ts +2 -2
  67. package/dist/transactions/distribution.d.ts.map +1 -1
  68. package/dist/transactions/distribution.js +7 -13
  69. package/dist/transactions/distribution.js.map +1 -1
  70. package/dist/transactions/gov.d.ts +2 -2
  71. package/dist/transactions/gov.d.ts.map +1 -1
  72. package/dist/transactions/gov.js +29 -20
  73. package/dist/transactions/gov.js.map +1 -1
  74. package/dist/transactions/index.d.ts +1 -1
  75. package/dist/transactions/index.d.ts.map +1 -1
  76. package/dist/transactions/index.js +1 -1
  77. package/dist/transactions/index.js.map +1 -1
  78. package/dist/transactions/manifest.d.ts +2 -2
  79. package/dist/transactions/manifest.d.ts.map +1 -1
  80. package/dist/transactions/manifest.js +7 -14
  81. package/dist/transactions/manifest.js.map +1 -1
  82. package/dist/transactions/sku.d.ts +7 -0
  83. package/dist/transactions/sku.d.ts.map +1 -0
  84. package/dist/transactions/sku.js +184 -0
  85. package/dist/transactions/sku.js.map +1 -0
  86. package/dist/transactions/staking.d.ts +2 -2
  87. package/dist/transactions/staking.d.ts.map +1 -1
  88. package/dist/transactions/staking.js +7 -13
  89. package/dist/transactions/staking.js.map +1 -1
  90. package/dist/transactions/utils.d.ts +65 -1
  91. package/dist/transactions/utils.d.ts.map +1 -1
  92. package/dist/transactions/utils.js +123 -2
  93. package/dist/transactions/utils.js.map +1 -1
  94. package/dist/transactions/utils.test.js +351 -1
  95. package/dist/transactions/utils.test.js.map +1 -1
  96. package/dist/types.d.ts +218 -8
  97. package/dist/types.d.ts.map +1 -1
  98. package/dist/types.js.map +1 -1
  99. package/dist/wallet/mnemonic.d.ts +1 -0
  100. package/dist/wallet/mnemonic.d.ts.map +1 -1
  101. package/dist/wallet/mnemonic.js +34 -13
  102. package/dist/wallet/mnemonic.js.map +1 -1
  103. package/package.json +5 -1
  104. package/src/client.ts +84 -21
  105. package/src/cosmos.ts +13 -109
  106. package/src/index.ts +17 -23
  107. package/src/modules.test.ts +62 -0
  108. package/src/modules.ts +155 -5
  109. package/src/queries/auth.ts +35 -74
  110. package/src/queries/bank.ts +40 -58
  111. package/src/queries/billing.ts +46 -86
  112. package/src/queries/distribution.ts +35 -59
  113. package/src/queries/gov.ts +40 -64
  114. package/src/queries/index.ts +10 -1
  115. package/src/queries/sku.ts +85 -0
  116. package/src/queries/staking.ts +55 -91
  117. package/src/queries/utils.test.ts +103 -8
  118. package/src/queries/utils.ts +119 -12
  119. package/src/transactions/bank.ts +9 -33
  120. package/src/transactions/billing.ts +155 -141
  121. package/src/transactions/distribution.ts +7 -29
  122. package/src/transactions/gov.ts +33 -37
  123. package/src/transactions/index.ts +1 -1
  124. package/src/transactions/manifest.ts +7 -29
  125. package/src/transactions/sku.ts +232 -0
  126. package/src/transactions/staking.ts +7 -29
  127. package/src/transactions/utils.test.ts +390 -1
  128. package/src/transactions/utils.ts +194 -2
  129. package/src/types.ts +344 -9
  130. package/src/wallet/mnemonic.ts +41 -17
  131. package/.claude/settings.local.json +0 -20
@@ -1,20 +1,113 @@
1
1
  import { ManifestMCPError, ManifestMCPErrorCode } from '../types.js';
2
- import { parseBigIntWithCode } from '../transactions/utils.js';
2
+ import { parseBigIntWithCode, requireArgs as requireArgsBase, extractFlag, filterConsumedArgs } from '../transactions/utils.js';
3
+
4
+ /**
5
+ * Result from extracting a boolean (valueless) flag from args
6
+ */
7
+ export interface ExtractedBooleanFlag {
8
+ /** Whether the flag was present */
9
+ value: boolean;
10
+ /** Args with the flag removed */
11
+ remainingArgs: string[];
12
+ }
13
+
14
+ /**
15
+ * Extract a valueless boolean flag from args array.
16
+ * Returns { value: true, remainingArgs } if flag is present, { value: false, remainingArgs: args } otherwise.
17
+ *
18
+ * @param args - The arguments array to search
19
+ * @param flagName - The flag to look for (e.g., '--active-only')
20
+ * @returns Object with boolean value and filtered args
21
+ */
22
+ export function extractBooleanFlag(args: string[], flagName: string): ExtractedBooleanFlag {
23
+ const flagIndex = args.indexOf(flagName);
24
+ if (flagIndex === -1) {
25
+ return { value: false, remainingArgs: args };
26
+ }
27
+ const remainingArgs = args.filter((_, index) => index !== flagIndex);
28
+ return { value: true, remainingArgs };
29
+ }
3
30
 
4
31
  /** Default page size limit for paginated queries to prevent resource exhaustion */
5
32
  export const DEFAULT_PAGE_LIMIT = BigInt(100);
6
33
 
34
+ /** Maximum page size limit to prevent DoS */
35
+ export const MAX_PAGE_LIMIT = BigInt(1000);
36
+
37
+ /**
38
+ * Cosmos SDK pagination configuration
39
+ */
40
+ export interface PaginationConfig {
41
+ readonly key: Uint8Array;
42
+ readonly offset: bigint;
43
+ readonly limit: bigint;
44
+ readonly countTotal: boolean;
45
+ readonly reverse: boolean;
46
+ }
47
+
48
+ /**
49
+ * Create pagination configuration with optional custom limit.
50
+ * Validates that limit is within acceptable bounds.
51
+ *
52
+ * @param limit - Optional custom limit (defaults to DEFAULT_PAGE_LIMIT)
53
+ * @returns Cosmos SDK pagination object
54
+ */
55
+ export function createPagination(limit?: bigint): PaginationConfig {
56
+ let effectiveLimit = limit ?? DEFAULT_PAGE_LIMIT;
57
+
58
+ // Clamp to valid range
59
+ if (effectiveLimit < BigInt(1)) {
60
+ effectiveLimit = BigInt(1);
61
+ } else if (effectiveLimit > MAX_PAGE_LIMIT) {
62
+ effectiveLimit = MAX_PAGE_LIMIT;
63
+ }
64
+
65
+ return {
66
+ key: new Uint8Array(),
67
+ offset: BigInt(0),
68
+ limit: effectiveLimit,
69
+ countTotal: false,
70
+ reverse: false,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Default pagination configuration for queries (for backwards compatibility)
76
+ * @deprecated Use createPagination() instead for configurable limits
77
+ */
78
+ export const defaultPagination = createPagination();
79
+
7
80
  /**
8
- * Default pagination configuration for queries
9
- * Uses Cosmos SDK pagination format
81
+ * Extract --limit flag from args and return pagination config with remaining args.
82
+ * Use this helper for paginated queries.
83
+ *
84
+ * @param args - The arguments array
85
+ * @param context - Context for error messages
86
+ * @returns Object with pagination config and filtered args
10
87
  */
11
- export const defaultPagination = {
12
- key: new Uint8Array(),
13
- offset: BigInt(0),
14
- limit: DEFAULT_PAGE_LIMIT,
15
- countTotal: false,
16
- reverse: false,
17
- };
88
+ export function extractPaginationArgs(
89
+ args: string[],
90
+ context: string
91
+ ): { pagination: PaginationConfig; remainingArgs: string[] } {
92
+ const { value: limitStr, consumedIndices } = extractFlag(args, '--limit', context);
93
+ const remainingArgs = filterConsumedArgs(args, consumedIndices);
94
+
95
+ let pagination: PaginationConfig;
96
+ if (limitStr) {
97
+ const limit = parseBigInt(limitStr, 'limit');
98
+ if (limit < BigInt(1) || limit > MAX_PAGE_LIMIT) {
99
+ throw new ManifestMCPError(
100
+ ManifestMCPErrorCode.QUERY_FAILED,
101
+ `Invalid limit: ${limit}. Must be between 1 and ${MAX_PAGE_LIMIT}.`
102
+ );
103
+ }
104
+ pagination = createPagination(limit);
105
+ } else {
106
+ pagination = createPagination();
107
+ }
108
+
109
+ return { pagination, remainingArgs };
110
+ }
18
111
 
19
112
  /**
20
113
  * Safely parse a string to BigInt with proper error handling (for queries)
@@ -24,9 +117,10 @@ export function parseBigInt(value: string, fieldName: string): bigint {
24
117
  }
25
118
 
26
119
  /**
27
- * Safely parse a string to integer with proper error handling
120
+ * Safely parse a string to integer with proper error handling.
121
+ * Named parseInteger to avoid shadowing global parseInt.
28
122
  */
29
- export function parseInt(value: string, fieldName: string): number {
123
+ export function parseInteger(value: string, fieldName: string): number {
30
124
  const parsed = Number.parseInt(value, 10);
31
125
  if (Number.isNaN(parsed)) {
32
126
  throw new ManifestMCPError(
@@ -36,3 +130,16 @@ export function parseInt(value: string, fieldName: string): number {
36
130
  }
37
131
  return parsed;
38
132
  }
133
+
134
+ /**
135
+ * Validate that required arguments are present (for queries).
136
+ * Uses QUERY_FAILED error code by default.
137
+ */
138
+ export function requireArgs(
139
+ args: string[],
140
+ minCount: number,
141
+ expectedNames: string[],
142
+ context: string
143
+ ): void {
144
+ requireArgsBase(args, minCount, expectedNames, context, ManifestMCPErrorCode.QUERY_FAILED);
145
+ }
@@ -1,7 +1,8 @@
1
1
  import { SigningStargateClient } from '@cosmjs/stargate';
2
2
  import { cosmos } from '@manifest-network/manifestjs';
3
- import { ManifestMCPError, ManifestMCPErrorCode, CosmosTxResult, ManifestMCPConfig } from '../types.js';
4
- import { parseAmount, buildTxResult, validateAddress, validateMemo, validateArgsLength } from './utils.js';
3
+ import { CosmosTxResult } from '../types.js';
4
+ import { throwUnsupportedSubcommand } from '../modules.js';
5
+ import { parseAmount, buildTxResult, validateAddress, validateMemo, validateArgsLength, extractFlag, parseColonPair, requireArgs } from './utils.js';
5
6
 
6
7
  const { MsgSend, MsgMultiSend } = cosmos.bank.v1beta1;
7
8
 
@@ -13,29 +14,20 @@ export async function routeBankTransaction(
13
14
  senderAddress: string,
14
15
  subcommand: string,
15
16
  args: string[],
16
- _config: ManifestMCPConfig,
17
17
  waitForConfirmation: boolean
18
18
  ): Promise<CosmosTxResult> {
19
19
  validateArgsLength(args, 'bank transaction');
20
20
 
21
21
  switch (subcommand) {
22
22
  case 'send': {
23
- if (args.length < 2) {
24
- throw new ManifestMCPError(
25
- ManifestMCPErrorCode.TX_FAILED,
26
- 'send requires recipient-address and amount arguments'
27
- );
28
- }
29
-
23
+ requireArgs(args, 2, ['recipient-address', 'amount'], 'bank send');
30
24
  const [recipientAddress, amountStr] = args;
31
25
  validateAddress(recipientAddress, 'recipient address');
32
26
  const { amount, denom } = parseAmount(amountStr);
33
27
 
34
28
  // Extract optional memo from args
35
- let memo = '';
36
- const memoIndex = args.indexOf('--memo');
37
- if (memoIndex !== -1 && args[memoIndex + 1]) {
38
- memo = args[memoIndex + 1];
29
+ const { value: memo = '' } = extractFlag(args, '--memo', 'bank send');
30
+ if (memo) {
39
31
  validateMemo(memo);
40
32
  }
41
33
 
@@ -53,22 +45,10 @@ export async function routeBankTransaction(
53
45
  }
54
46
 
55
47
  case 'multi-send': {
56
- if (args.length < 2) {
57
- throw new ManifestMCPError(
58
- ManifestMCPErrorCode.TX_FAILED,
59
- 'multi-send requires at least one recipient:amount pair'
60
- );
61
- }
62
-
48
+ requireArgs(args, 1, ['recipient:amount'], 'bank multi-send');
63
49
  // Parse format: multi-send recipient1:amount1 recipient2:amount2 ...
64
50
  const outputs = args.map((arg) => {
65
- const [address, amountStr] = arg.split(':');
66
- if (!address || !amountStr) {
67
- throw new ManifestMCPError(
68
- ManifestMCPErrorCode.TX_FAILED,
69
- `Invalid multi-send format: ${arg}. Expected format: address:amount`
70
- );
71
- }
51
+ const [address, amountStr] = parseColonPair(arg, 'address', 'amount', 'multi-send');
72
52
  validateAddress(address, 'recipient address');
73
53
  const { amount, denom } = parseAmount(amountStr);
74
54
  return { address, coins: [{ denom, amount }] };
@@ -101,10 +81,6 @@ export async function routeBankTransaction(
101
81
  }
102
82
 
103
83
  default:
104
- throw new ManifestMCPError(
105
- ManifestMCPErrorCode.UNSUPPORTED_TX,
106
- `Unsupported bank transaction subcommand: ${subcommand}`,
107
- { availableSubcommands: ['send', 'multi-send'] }
108
- );
84
+ throwUnsupportedSubcommand('tx', 'bank', subcommand);
109
85
  }
110
86
  }
@@ -1,46 +1,14 @@
1
1
  import { SigningStargateClient } from '@cosmjs/stargate';
2
- import { fromHex } from '@cosmjs/encoding';
3
2
  import { liftedinit } from '@manifest-network/manifestjs';
4
- import { ManifestMCPError, ManifestMCPErrorCode, CosmosTxResult, ManifestMCPConfig } from '../types.js';
5
- import { parseAmount, buildTxResult, parseBigInt, validateAddress, validateArgsLength } from './utils.js';
6
- import { getSubcommandUsage } from '../modules.js';
3
+ import { CosmosTxResult, ManifestMCPError, ManifestMCPErrorCode } from '../types.js';
4
+ import { parseAmount, buildTxResult, parseBigInt, validateAddress, validateArgsLength, extractFlag, filterConsumedArgs, parseColonPair, requireArgs, parseHexBytes, MAX_META_HASH_BYTES } from './utils.js';
5
+ import { getSubcommandUsage, throwUnsupportedSubcommand } from '../modules.js';
7
6
 
8
- const { MsgFundCredit, MsgCreateLease, MsgCloseLease, MsgWithdraw } = liftedinit.billing.v1;
9
-
10
- /** Maximum meta hash length in bytes (64 bytes for SHA-512) */
11
- const MAX_META_HASH_BYTES = 64;
12
-
13
- /**
14
- * Validate and parse a hex string into Uint8Array
15
- * Uses @cosmjs/encoding for hex validation and conversion
16
- */
17
- function parseMetaHash(hexString: string): Uint8Array {
18
- // Check even length first to avoid fractional byte counts in error messages
19
- if (hexString.length % 2 !== 0) {
20
- throw new ManifestMCPError(
21
- ManifestMCPErrorCode.TX_FAILED,
22
- `Invalid meta-hash: hex string must have even length. Got ${hexString.length} characters.`
23
- );
24
- }
25
-
26
- // Check max length (64 bytes = 128 hex chars)
27
- const byteLength = hexString.length / 2;
28
- if (byteLength > MAX_META_HASH_BYTES) {
29
- throw new ManifestMCPError(
30
- ManifestMCPErrorCode.TX_FAILED,
31
- `Invalid meta-hash: exceeds maximum ${MAX_META_HASH_BYTES} bytes. Got ${byteLength} bytes (${hexString.length} hex chars).`
32
- );
33
- }
34
-
35
- try {
36
- return fromHex(hexString);
37
- } catch (error) {
38
- throw new ManifestMCPError(
39
- ManifestMCPErrorCode.TX_FAILED,
40
- `Invalid meta-hash: ${error instanceof Error ? error.message : String(error)}`
41
- );
42
- }
43
- }
7
+ const {
8
+ MsgFundCredit, MsgCreateLease, MsgCloseLease, MsgWithdraw,
9
+ MsgCreateLeaseForTenant, MsgAcknowledgeLease, MsgRejectLease, MsgCancelLease,
10
+ MsgUpdateParams,
11
+ } = liftedinit.billing.v1;
44
12
 
45
13
  /**
46
14
  * Route billing transaction to appropriate handler
@@ -50,22 +18,13 @@ export async function routeBillingTransaction(
50
18
  senderAddress: string,
51
19
  subcommand: string,
52
20
  args: string[],
53
- _config: ManifestMCPConfig,
54
21
  waitForConfirmation: boolean
55
22
  ): Promise<CosmosTxResult> {
56
23
  validateArgsLength(args, 'billing transaction');
57
24
 
58
25
  switch (subcommand) {
59
26
  case 'fund-credit': {
60
- if (args.length < 2) {
61
- const usage = getSubcommandUsage('tx', 'billing', 'fund-credit');
62
- throw new ManifestMCPError(
63
- ManifestMCPErrorCode.TX_FAILED,
64
- `fund-credit requires tenant-address and amount arguments. Received ${args.length} argument(s): [${args.map(a => `"${a}"`).join(', ')}]. Usage: fund-credit ${usage || '<tenant-address> <amount>'}`,
65
- { receivedArgs: args, expectedArgs: ['tenant-address', 'amount'], usage }
66
- );
67
- }
68
-
27
+ requireArgs(args, 2, ['tenant-address', 'amount'], 'billing fund-credit');
69
28
  const [tenant, amountStr] = args;
70
29
  validateAddress(tenant, 'tenant address');
71
30
  const { amount, denom } = parseAmount(amountStr);
@@ -84,42 +43,17 @@ export async function routeBillingTransaction(
84
43
  }
85
44
 
86
45
  case 'create-lease': {
87
- // Parse optional --meta-hash flag
88
- let metaHash: Uint8Array | undefined;
89
- let itemArgs = args;
46
+ // Parse optional --meta-hash flag (can appear anywhere in args)
47
+ const { value: metaHashHex, consumedIndices } = extractFlag(args, '--meta-hash', 'billing create-lease');
48
+ const metaHash = metaHashHex ? parseHexBytes(metaHashHex, 'meta-hash', MAX_META_HASH_BYTES) : undefined;
90
49
 
91
- if (args[0] === '--meta-hash') {
92
- if (args.length < 2 || args[1].startsWith('--')) {
93
- const usage = getSubcommandUsage('tx', 'billing', 'create-lease');
94
- throw new ManifestMCPError(
95
- ManifestMCPErrorCode.TX_FAILED,
96
- `--meta-hash flag requires a hex value. Usage: create-lease ${usage ?? '<args>'}`
97
- );
98
- }
99
- const hexHash = args[1];
100
- // Validate and convert hex string to Uint8Array (max 64 bytes)
101
- metaHash = parseMetaHash(hexHash);
102
- itemArgs = args.slice(2);
103
- }
104
-
105
- if (itemArgs.length < 1) {
106
- const usage = getSubcommandUsage('tx', 'billing', 'create-lease');
107
- throw new ManifestMCPError(
108
- ManifestMCPErrorCode.TX_FAILED,
109
- `create-lease requires at least one sku-uuid:quantity pair. Usage: create-lease ${usage ?? '<args>'}`,
110
- { usage }
111
- );
112
- }
50
+ // Filter out --meta-hash and its value to get item args
51
+ const itemArgs = filterConsumedArgs(args, consumedIndices);
52
+ requireArgs(itemArgs, 1, ['sku-uuid:quantity'], 'billing create-lease');
113
53
 
114
54
  // Parse items (format: sku-uuid:quantity ...)
115
55
  const items = itemArgs.map((arg) => {
116
- const [skuUuid, quantityStr] = arg.split(':');
117
- if (!skuUuid || !quantityStr) {
118
- throw new ManifestMCPError(
119
- ManifestMCPErrorCode.TX_FAILED,
120
- `Invalid lease item format: ${arg}. Expected format: sku-uuid:quantity`
121
- );
122
- }
56
+ const [skuUuid, quantityStr] = parseColonPair(arg, 'sku-uuid', 'quantity', 'lease item');
123
57
  return { skuUuid, quantity: parseBigInt(quantityStr, 'quantity') };
124
58
  });
125
59
 
@@ -137,25 +71,20 @@ export async function routeBillingTransaction(
137
71
  }
138
72
 
139
73
  case 'close-lease': {
140
- if (args.length < 1) {
141
- const usage = getSubcommandUsage('tx', 'billing', 'close-lease');
142
- throw new ManifestMCPError(
143
- ManifestMCPErrorCode.TX_FAILED,
144
- `close-lease requires at least one lease-uuid argument. Usage: close-lease ${usage || '<lease-uuid>...'}`,
145
- { usage }
146
- );
147
- }
74
+ // Parse optional --reason flag
75
+ const { value: reason, consumedIndices } = extractFlag(args, '--reason', 'billing close-lease');
76
+ const leaseArgs = filterConsumedArgs(args, consumedIndices);
77
+ requireArgs(leaseArgs, 1, ['lease-uuid'], 'billing close-lease');
148
78
 
149
79
  // MsgCloseLease can close multiple leases at once
150
- const leaseUuids = args;
151
- const reason = ''; // Optional reason, could be added as a flag later
80
+ const leaseUuids = leaseArgs;
152
81
 
153
82
  const msg = {
154
83
  typeUrl: '/liftedinit.billing.v1.MsgCloseLease',
155
84
  value: MsgCloseLease.fromPartial({
156
85
  sender: senderAddress,
157
86
  leaseUuids,
158
- reason,
87
+ reason: reason ?? '',
159
88
  }),
160
89
  };
161
90
 
@@ -164,57 +93,34 @@ export async function routeBillingTransaction(
164
93
  }
165
94
 
166
95
  case 'withdraw': {
167
- if (args.length < 1) {
168
- const usage = getSubcommandUsage('tx', 'billing', 'withdraw');
169
- throw new ManifestMCPError(
170
- ManifestMCPErrorCode.TX_FAILED,
171
- `withdraw requires at least one lease-uuid argument or provider-uuid with --provider flag. Usage: withdraw ${usage ?? '<args>'}`,
172
- { usage }
173
- );
174
- }
96
+ requireArgs(args, 1, ['lease-uuid or --provider'], 'billing withdraw');
97
+
98
+ // Extract flags
99
+ const providerFlag = extractFlag(args, '--provider', 'billing withdraw');
100
+ const limitFlag = extractFlag(args, '--limit', 'billing withdraw');
175
101
 
176
- // Check if using provider-wide withdrawal
177
- const providerFlagIndex = args.indexOf('--provider');
178
- const limitFlagIndex = args.indexOf('--limit');
179
102
  let leaseUuids: string[] = [];
180
103
  let providerUuid = '';
181
104
  let limit = BigInt(0); // 0 means use default (50)
182
105
 
183
- if (providerFlagIndex !== -1) {
106
+ if (providerFlag.value) {
184
107
  // Provider-wide withdrawal mode
185
- providerUuid = args[providerFlagIndex + 1] || '';
186
- if (!providerUuid || providerUuid.startsWith('--')) {
187
- throw new ManifestMCPError(
188
- ManifestMCPErrorCode.TX_FAILED,
189
- 'withdraw with --provider flag requires provider-uuid argument'
190
- );
191
- }
192
-
193
- // Track consumed arg indices to detect extra arguments
194
- const consumedIndices = new Set<number>([providerFlagIndex, providerFlagIndex + 1]);
108
+ providerUuid = providerFlag.value;
195
109
 
196
110
  // Parse optional --limit flag (only valid with --provider)
197
- if (limitFlagIndex !== -1) {
198
- const limitStr = args[limitFlagIndex + 1] || '';
199
- if (!limitStr || limitStr.startsWith('--')) {
200
- throw new ManifestMCPError(
201
- ManifestMCPErrorCode.TX_FAILED,
202
- 'withdraw with --limit flag requires a number argument (1-100)'
203
- );
204
- }
205
- limit = parseBigInt(limitStr, 'limit');
111
+ if (limitFlag.value) {
112
+ limit = parseBigInt(limitFlag.value, 'limit');
206
113
  if (limit < BigInt(1) || limit > BigInt(100)) {
207
114
  throw new ManifestMCPError(
208
115
  ManifestMCPErrorCode.TX_FAILED,
209
116
  `Invalid limit: ${limit}. Must be between 1 and 100.`
210
117
  );
211
118
  }
212
- consumedIndices.add(limitFlagIndex);
213
- consumedIndices.add(limitFlagIndex + 1);
214
119
  }
215
120
 
216
121
  // Check for any extra arguments that weren't consumed
217
- const extraArgs = args.filter((_, index) => !consumedIndices.has(index));
122
+ const allConsumed = [...providerFlag.consumedIndices, ...limitFlag.consumedIndices];
123
+ const extraArgs = filterConsumedArgs(args, allConsumed);
218
124
  if (extraArgs.length > 0) {
219
125
  const usage = getSubcommandUsage('tx', 'billing', 'withdraw');
220
126
  throw new ManifestMCPError(
@@ -226,7 +132,7 @@ export async function routeBillingTransaction(
226
132
  }
227
133
  } else {
228
134
  // Lease-specific withdrawal mode
229
- // Check for unexpected flags
135
+ // Check for unexpected flags (--limit without --provider is invalid)
230
136
  const unexpectedFlags = args.filter(arg => arg.startsWith('--'));
231
137
  if (unexpectedFlags.length > 0) {
232
138
  const usage = getSubcommandUsage('tx', 'billing', 'withdraw');
@@ -238,14 +144,6 @@ export async function routeBillingTransaction(
238
144
  }
239
145
 
240
146
  leaseUuids = args;
241
-
242
- if (leaseUuids.length === 0) {
243
- const usage = getSubcommandUsage('tx', 'billing', 'withdraw');
244
- throw new ManifestMCPError(
245
- ManifestMCPErrorCode.TX_FAILED,
246
- `withdraw requires at least one lease-uuid. Usage: withdraw ${usage ?? '<args>'}`
247
- );
248
- }
249
147
  }
250
148
 
251
149
  const msg = {
@@ -262,11 +160,127 @@ export async function routeBillingTransaction(
262
160
  return buildTxResult('billing', 'withdraw', result, waitForConfirmation);
263
161
  }
264
162
 
163
+ case 'create-lease-for-tenant': {
164
+ // Parse optional --meta-hash flag
165
+ const { value: metaHashHex, consumedIndices } = extractFlag(args, '--meta-hash', 'billing create-lease-for-tenant');
166
+ const metaHash = metaHashHex ? parseHexBytes(metaHashHex, 'meta-hash', MAX_META_HASH_BYTES) : undefined;
167
+
168
+ // Filter out --meta-hash and its value to get remaining args
169
+ const remainingArgs = filterConsumedArgs(args, consumedIndices);
170
+ requireArgs(remainingArgs, 2, ['tenant-address', 'sku-uuid:quantity'], 'billing create-lease-for-tenant');
171
+
172
+ const [tenant, ...itemArgs] = remainingArgs;
173
+ validateAddress(tenant, 'tenant address');
174
+
175
+ // Parse items (format: sku-uuid:quantity ...)
176
+ const items = itemArgs.map((arg) => {
177
+ const [skuUuid, quantityStr] = parseColonPair(arg, 'sku-uuid', 'quantity', 'lease item');
178
+ return { skuUuid, quantity: parseBigInt(quantityStr, 'quantity') };
179
+ });
180
+
181
+ const msg = {
182
+ typeUrl: '/liftedinit.billing.v1.MsgCreateLeaseForTenant',
183
+ value: MsgCreateLeaseForTenant.fromPartial({
184
+ authority: senderAddress,
185
+ tenant,
186
+ items,
187
+ metaHash: metaHash ?? new Uint8Array(),
188
+ }),
189
+ };
190
+
191
+ const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
192
+ return buildTxResult('billing', 'create-lease-for-tenant', result, waitForConfirmation);
193
+ }
194
+
195
+ case 'acknowledge-lease': {
196
+ requireArgs(args, 1, ['lease-uuid'], 'billing acknowledge-lease');
197
+ const leaseUuids = args;
198
+
199
+ const msg = {
200
+ typeUrl: '/liftedinit.billing.v1.MsgAcknowledgeLease',
201
+ value: MsgAcknowledgeLease.fromPartial({
202
+ sender: senderAddress,
203
+ leaseUuids,
204
+ }),
205
+ };
206
+
207
+ const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
208
+ return buildTxResult('billing', 'acknowledge-lease', result, waitForConfirmation);
209
+ }
210
+
211
+ case 'reject-lease': {
212
+ // Parse optional --reason flag
213
+ const { value: reason, consumedIndices } = extractFlag(args, '--reason', 'billing reject-lease');
214
+ const leaseArgs = filterConsumedArgs(args, consumedIndices);
215
+ requireArgs(leaseArgs, 1, ['lease-uuid'], 'billing reject-lease');
216
+
217
+ const leaseUuids = leaseArgs;
218
+
219
+ const msg = {
220
+ typeUrl: '/liftedinit.billing.v1.MsgRejectLease',
221
+ value: MsgRejectLease.fromPartial({
222
+ sender: senderAddress,
223
+ leaseUuids,
224
+ reason: reason ?? '',
225
+ }),
226
+ };
227
+
228
+ const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
229
+ return buildTxResult('billing', 'reject-lease', result, waitForConfirmation);
230
+ }
231
+
232
+ case 'cancel-lease': {
233
+ requireArgs(args, 1, ['lease-uuid'], 'billing cancel-lease');
234
+ const leaseUuids = args;
235
+
236
+ const msg = {
237
+ typeUrl: '/liftedinit.billing.v1.MsgCancelLease',
238
+ value: MsgCancelLease.fromPartial({
239
+ tenant: senderAddress,
240
+ leaseUuids,
241
+ }),
242
+ };
243
+
244
+ const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
245
+ return buildTxResult('billing', 'cancel-lease', result, waitForConfirmation);
246
+ }
247
+
248
+ case 'update-params': {
249
+ requireArgs(args, 5, [
250
+ 'max-leases-per-tenant', 'max-items-per-lease', 'min-lease-duration',
251
+ 'max-pending-leases-per-tenant', 'pending-timeout',
252
+ ], 'billing update-params');
253
+
254
+ const [
255
+ maxLeasesPerTenantStr, maxItemsPerLeaseStr, minLeaseDurationStr,
256
+ maxPendingLeasesPerTenantStr, pendingTimeoutStr,
257
+ ...allowedAddresses
258
+ ] = args;
259
+
260
+ for (const addr of allowedAddresses) {
261
+ validateAddress(addr, 'allowed address');
262
+ }
263
+
264
+ const msg = {
265
+ typeUrl: '/liftedinit.billing.v1.MsgUpdateParams',
266
+ value: MsgUpdateParams.fromPartial({
267
+ authority: senderAddress,
268
+ params: {
269
+ maxLeasesPerTenant: parseBigInt(maxLeasesPerTenantStr, 'max-leases-per-tenant'),
270
+ maxItemsPerLease: parseBigInt(maxItemsPerLeaseStr, 'max-items-per-lease'),
271
+ minLeaseDuration: parseBigInt(minLeaseDurationStr, 'min-lease-duration'),
272
+ maxPendingLeasesPerTenant: parseBigInt(maxPendingLeasesPerTenantStr, 'max-pending-leases-per-tenant'),
273
+ pendingTimeout: parseBigInt(pendingTimeoutStr, 'pending-timeout'),
274
+ allowedList: allowedAddresses,
275
+ },
276
+ }),
277
+ };
278
+
279
+ const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
280
+ return buildTxResult('billing', 'update-params', result, waitForConfirmation);
281
+ }
282
+
265
283
  default:
266
- throw new ManifestMCPError(
267
- ManifestMCPErrorCode.UNSUPPORTED_TX,
268
- `Unsupported billing transaction subcommand: ${subcommand}`,
269
- { availableSubcommands: ['fund-credit', 'create-lease', 'close-lease', 'withdraw'] }
270
- );
284
+ throwUnsupportedSubcommand('tx', 'billing', subcommand);
271
285
  }
272
286
  }