@manifest-network/manifest-mcp-browser 0.1.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/.claude/settings.local.json +17 -0
- package/.github/workflows/ci.yml +37 -0
- package/.github/workflows/publish.yml +51 -0
- package/CLAUDE.md +104 -0
- package/LICENSE +21 -0
- package/README.md +298 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js.map +1 -0
- package/dist/client.d.ts +44 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +131 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +98 -0
- package/dist/config.js.map +1 -0
- package/dist/config.test.d.ts +2 -0
- package/dist/config.test.d.ts.map +1 -0
- package/dist/config.test.js +123 -0
- package/dist/config.test.js.map +1 -0
- package/dist/cosmos.d.ts +11 -0
- package/dist/cosmos.d.ts.map +1 -0
- package/dist/cosmos.js +112 -0
- package/dist/cosmos.js.map +1 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +382 -0
- package/dist/index.js.map +1 -0
- package/dist/modules.d.ts +30 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +221 -0
- package/dist/modules.js.map +1 -0
- package/dist/modules.test.d.ts +2 -0
- package/dist/modules.test.d.ts.map +1 -0
- package/dist/modules.test.js +100 -0
- package/dist/modules.test.js.map +1 -0
- package/dist/queries/auth.d.ts +6 -0
- package/dist/queries/auth.d.ts.map +1 -0
- package/dist/queries/auth.js +93 -0
- package/dist/queries/auth.js.map +1 -0
- package/dist/queries/bank.d.ts +6 -0
- package/dist/queries/bank.d.ts.map +1 -0
- package/dist/queries/bank.js +83 -0
- package/dist/queries/bank.js.map +1 -0
- package/dist/queries/billing.d.ts +6 -0
- package/dist/queries/billing.d.ts.map +1 -0
- package/dist/queries/billing.js +115 -0
- package/dist/queries/billing.js.map +1 -0
- package/dist/queries/distribution.d.ts +6 -0
- package/dist/queries/distribution.d.ts.map +1 -0
- package/dist/queries/distribution.js +102 -0
- package/dist/queries/distribution.js.map +1 -0
- package/dist/queries/gov.d.ts +6 -0
- package/dist/queries/gov.d.ts.map +1 -0
- package/dist/queries/gov.js +92 -0
- package/dist/queries/gov.js.map +1 -0
- package/dist/queries/index.d.ts +8 -0
- package/dist/queries/index.d.ts.map +1 -0
- package/dist/queries/index.js +8 -0
- package/dist/queries/index.js.map +1 -0
- package/dist/queries/manifest.d.ts +10 -0
- package/dist/queries/manifest.d.ts.map +1 -0
- package/dist/queries/manifest.js +14 -0
- package/dist/queries/manifest.js.map +1 -0
- package/dist/queries/staking.d.ts +6 -0
- package/dist/queries/staking.d.ts.map +1 -0
- package/dist/queries/staking.js +141 -0
- package/dist/queries/staking.js.map +1 -0
- package/dist/queries/utils.d.ts +22 -0
- package/dist/queries/utils.d.ts.map +1 -0
- package/dist/queries/utils.js +32 -0
- package/dist/queries/utils.js.map +1 -0
- package/dist/queries/utils.test.d.ts +2 -0
- package/dist/queries/utils.test.d.ts.map +1 -0
- package/dist/queries/utils.test.js +57 -0
- package/dist/queries/utils.test.js.map +1 -0
- package/dist/transactions/bank.d.ts +7 -0
- package/dist/transactions/bank.d.ts.map +1 -0
- package/dist/transactions/bank.js +76 -0
- package/dist/transactions/bank.js.map +1 -0
- package/dist/transactions/billing.d.ts +7 -0
- package/dist/transactions/billing.d.ts.map +1 -0
- package/dist/transactions/billing.js +108 -0
- package/dist/transactions/billing.js.map +1 -0
- package/dist/transactions/distribution.d.ts +7 -0
- package/dist/transactions/distribution.d.ts.map +1 -0
- package/dist/transactions/distribution.js +63 -0
- package/dist/transactions/distribution.js.map +1 -0
- package/dist/transactions/gov.d.ts +7 -0
- package/dist/transactions/gov.d.ts.map +1 -0
- package/dist/transactions/gov.js +132 -0
- package/dist/transactions/gov.js.map +1 -0
- package/dist/transactions/index.d.ts +8 -0
- package/dist/transactions/index.d.ts.map +1 -0
- package/dist/transactions/index.js +8 -0
- package/dist/transactions/index.js.map +1 -0
- package/dist/transactions/manifest.d.ts +7 -0
- package/dist/transactions/manifest.d.ts.map +1 -0
- package/dist/transactions/manifest.js +58 -0
- package/dist/transactions/manifest.js.map +1 -0
- package/dist/transactions/staking.d.ts +7 -0
- package/dist/transactions/staking.d.ts.map +1 -0
- package/dist/transactions/staking.js +72 -0
- package/dist/transactions/staking.js.map +1 -0
- package/dist/transactions/utils.d.ts +40 -0
- package/dist/transactions/utils.d.ts.map +1 -0
- package/dist/transactions/utils.js +114 -0
- package/dist/transactions/utils.js.map +1 -0
- package/dist/transactions/utils.test.d.ts +2 -0
- package/dist/transactions/utils.test.d.ts.map +1 -0
- package/dist/transactions/utils.test.js +121 -0
- package/dist/transactions/utils.test.js.map +1 -0
- package/dist/types.d.ts +110 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +55 -0
- package/dist/types.js.map +1 -0
- package/dist/wallet/index.d.ts +3 -0
- package/dist/wallet/index.d.ts.map +1 -0
- package/dist/wallet/index.js +2 -0
- package/dist/wallet/index.js.map +1 -0
- package/dist/wallet/keplr.d.ts.map +1 -0
- package/dist/wallet/keplr.js.map +1 -0
- package/dist/wallet/mnemonic.d.ts +40 -0
- package/dist/wallet/mnemonic.d.ts.map +1 -0
- package/dist/wallet/mnemonic.js +87 -0
- package/dist/wallet/mnemonic.js.map +1 -0
- package/package.json +40 -0
- package/src/client.ts +178 -0
- package/src/config.test.ts +143 -0
- package/src/config.ts +122 -0
- package/src/cosmos.ts +196 -0
- package/src/index.ts +484 -0
- package/src/modules.test.ts +127 -0
- package/src/modules.ts +278 -0
- package/src/queries/auth.ts +136 -0
- package/src/queries/bank.ts +117 -0
- package/src/queries/billing.ts +164 -0
- package/src/queries/distribution.ts +138 -0
- package/src/queries/gov.ts +128 -0
- package/src/queries/index.ts +7 -0
- package/src/queries/staking.ts +190 -0
- package/src/queries/utils.test.ts +61 -0
- package/src/queries/utils.ts +38 -0
- package/src/transactions/bank.ts +110 -0
- package/src/transactions/billing.ts +160 -0
- package/src/transactions/distribution.ts +98 -0
- package/src/transactions/gov.ts +185 -0
- package/src/transactions/index.ts +7 -0
- package/src/transactions/manifest.ts +89 -0
- package/src/transactions/staking.ts +107 -0
- package/src/transactions/utils.test.ts +129 -0
- package/src/transactions/utils.ts +156 -0
- package/src/types.ts +152 -0
- package/src/wallet/index.ts +2 -0
- package/src/wallet/mnemonic.ts +122 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseAmount, parseBigInt } from './utils.js';
|
|
3
|
+
import { ManifestMCPError, ManifestMCPErrorCode } from '../types.js';
|
|
4
|
+
|
|
5
|
+
describe('parseAmount', () => {
|
|
6
|
+
it('should parse valid amount strings', () => {
|
|
7
|
+
expect(parseAmount('1000umfx')).toEqual({ amount: '1000', denom: 'umfx' });
|
|
8
|
+
expect(parseAmount('1uatom')).toEqual({ amount: '1', denom: 'uatom' });
|
|
9
|
+
expect(parseAmount('999999999token')).toEqual({ amount: '999999999', denom: 'token' });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should handle denominations with numbers', () => {
|
|
13
|
+
expect(parseAmount('100ibc123')).toEqual({ amount: '100', denom: 'ibc123' });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should handle factory denoms with slashes', () => {
|
|
17
|
+
expect(parseAmount('1000000factory/manifest1abc123/upwr')).toEqual({
|
|
18
|
+
amount: '1000000',
|
|
19
|
+
denom: 'factory/manifest1abc123/upwr',
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should handle IBC denoms with slashes', () => {
|
|
24
|
+
expect(parseAmount('500ibc/ABC123DEF456')).toEqual({
|
|
25
|
+
amount: '500',
|
|
26
|
+
denom: 'ibc/ABC123DEF456',
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should handle denoms with underscores', () => {
|
|
31
|
+
expect(parseAmount('100my_token')).toEqual({ amount: '100', denom: 'my_token' });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should throw ManifestMCPError for invalid format', () => {
|
|
35
|
+
expect(() => parseAmount('')).toThrow(ManifestMCPError);
|
|
36
|
+
expect(() => parseAmount('umfx')).toThrow(ManifestMCPError);
|
|
37
|
+
expect(() => parseAmount('1000')).toThrow(ManifestMCPError);
|
|
38
|
+
expect(() => parseAmount('abc123')).toThrow(ManifestMCPError);
|
|
39
|
+
expect(() => parseAmount('1000 umfx')).toThrow(ManifestMCPError);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should have correct error code for invalid format', () => {
|
|
43
|
+
try {
|
|
44
|
+
parseAmount('invalid');
|
|
45
|
+
} catch (error) {
|
|
46
|
+
expect(error).toBeInstanceOf(ManifestMCPError);
|
|
47
|
+
expect((error as ManifestMCPError).code).toBe(ManifestMCPErrorCode.TX_FAILED);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should provide helpful hint for empty string', () => {
|
|
52
|
+
try {
|
|
53
|
+
parseAmount('');
|
|
54
|
+
} catch (error) {
|
|
55
|
+
expect((error as ManifestMCPError).message).toContain('Received empty string');
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should provide helpful hint for amount with space', () => {
|
|
60
|
+
try {
|
|
61
|
+
parseAmount('1000 umfx');
|
|
62
|
+
} catch (error) {
|
|
63
|
+
expect((error as ManifestMCPError).message).toContain('Remove the space');
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should provide helpful hint for amount with comma', () => {
|
|
68
|
+
try {
|
|
69
|
+
parseAmount('1,000umfx');
|
|
70
|
+
} catch (error) {
|
|
71
|
+
expect((error as ManifestMCPError).message).toContain('Do not use commas');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should provide helpful hint for missing denomination', () => {
|
|
76
|
+
try {
|
|
77
|
+
parseAmount('1000');
|
|
78
|
+
} catch (error) {
|
|
79
|
+
expect((error as ManifestMCPError).message).toContain('Missing denomination');
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should provide helpful hint for denom-first format', () => {
|
|
84
|
+
try {
|
|
85
|
+
parseAmount('umfx1000');
|
|
86
|
+
} catch (error) {
|
|
87
|
+
expect((error as ManifestMCPError).message).toContain('must start with a number');
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should include details with received value and example', () => {
|
|
92
|
+
try {
|
|
93
|
+
parseAmount('bad');
|
|
94
|
+
} catch (error) {
|
|
95
|
+
const details = (error as ManifestMCPError).details;
|
|
96
|
+
expect(details?.receivedValue).toBe('bad');
|
|
97
|
+
expect(details?.example).toBe('1000000umfx');
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('parseBigInt', () => {
|
|
103
|
+
it('should parse valid integer strings', () => {
|
|
104
|
+
expect(parseBigInt('0', 'test')).toBe(BigInt(0));
|
|
105
|
+
expect(parseBigInt('123', 'test')).toBe(BigInt(123));
|
|
106
|
+
expect(parseBigInt('9999999999999999999', 'test')).toBe(BigInt('9999999999999999999'));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should throw ManifestMCPError for invalid integers', () => {
|
|
110
|
+
expect(() => parseBigInt('abc', 'field')).toThrow(ManifestMCPError);
|
|
111
|
+
expect(() => parseBigInt('12.34', 'field')).toThrow(ManifestMCPError);
|
|
112
|
+
expect(() => parseBigInt('1e10', 'field')).toThrow(ManifestMCPError);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should throw ManifestMCPError for empty string', () => {
|
|
116
|
+
// Empty string should be rejected for security (prevents accidental 0 values)
|
|
117
|
+
expect(() => parseBigInt('', 'field')).toThrow(ManifestMCPError);
|
|
118
|
+
expect(() => parseBigInt(' ', 'field')).toThrow(ManifestMCPError);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should include field name in error message', () => {
|
|
122
|
+
try {
|
|
123
|
+
parseBigInt('invalid', 'proposal-id');
|
|
124
|
+
} catch (error) {
|
|
125
|
+
expect(error).toBeInstanceOf(ManifestMCPError);
|
|
126
|
+
expect((error as ManifestMCPError).message).toContain('proposal-id');
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { SigningStargateClient } from '@cosmjs/stargate';
|
|
2
|
+
import { fromBech32 } from '@cosmjs/encoding';
|
|
3
|
+
import { ManifestMCPError, ManifestMCPErrorCode, CosmosTxResult } from '../types.js';
|
|
4
|
+
|
|
5
|
+
/** Maximum number of arguments allowed */
|
|
6
|
+
export const MAX_ARGS = 100;
|
|
7
|
+
|
|
8
|
+
/** Maximum memo length (Cosmos SDK default) */
|
|
9
|
+
export const MAX_MEMO_LENGTH = 256;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate args array length
|
|
13
|
+
*/
|
|
14
|
+
export function validateArgsLength(args: string[], context: string): void {
|
|
15
|
+
if (args.length > MAX_ARGS) {
|
|
16
|
+
throw new ManifestMCPError(
|
|
17
|
+
ManifestMCPErrorCode.TX_FAILED,
|
|
18
|
+
`Too many arguments for ${context}: ${args.length}. Maximum allowed: ${MAX_ARGS}`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validate a bech32 address using @cosmjs/encoding
|
|
25
|
+
*/
|
|
26
|
+
export function validateAddress(address: string, fieldName: string, expectedPrefix?: string): void {
|
|
27
|
+
if (!address || address.trim() === '') {
|
|
28
|
+
throw new ManifestMCPError(
|
|
29
|
+
ManifestMCPErrorCode.INVALID_ADDRESS,
|
|
30
|
+
`${fieldName} is required`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const { prefix } = fromBech32(address);
|
|
36
|
+
if (expectedPrefix && prefix !== expectedPrefix) {
|
|
37
|
+
throw new ManifestMCPError(
|
|
38
|
+
ManifestMCPErrorCode.INVALID_ADDRESS,
|
|
39
|
+
`Invalid ${fieldName}: "${address}". Expected prefix "${expectedPrefix}", got "${prefix}"`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error instanceof ManifestMCPError) {
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
throw new ManifestMCPError(
|
|
47
|
+
ManifestMCPErrorCode.INVALID_ADDRESS,
|
|
48
|
+
`Invalid ${fieldName}: "${address}". Not a valid bech32 address.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validate memo length
|
|
55
|
+
*/
|
|
56
|
+
export function validateMemo(memo: string): void {
|
|
57
|
+
if (memo.length > MAX_MEMO_LENGTH) {
|
|
58
|
+
throw new ManifestMCPError(
|
|
59
|
+
ManifestMCPErrorCode.TX_FAILED,
|
|
60
|
+
`Memo too long: ${memo.length} characters. Maximum allowed: ${MAX_MEMO_LENGTH}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Safely parse a string to BigInt with proper error handling and configurable error code.
|
|
67
|
+
* This is the base implementation used by both transaction and query utilities.
|
|
68
|
+
*/
|
|
69
|
+
export function parseBigIntWithCode(
|
|
70
|
+
value: string,
|
|
71
|
+
fieldName: string,
|
|
72
|
+
errorCode: ManifestMCPErrorCode
|
|
73
|
+
): bigint {
|
|
74
|
+
// Check for empty string explicitly (BigInt('') returns 0n, not an error)
|
|
75
|
+
if (!value || value.trim() === '') {
|
|
76
|
+
throw new ManifestMCPError(
|
|
77
|
+
errorCode,
|
|
78
|
+
`Invalid ${fieldName}: empty value. Expected a valid integer.`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
return BigInt(value);
|
|
84
|
+
} catch {
|
|
85
|
+
throw new ManifestMCPError(
|
|
86
|
+
errorCode,
|
|
87
|
+
`Invalid ${fieldName}: "${value}". Expected a valid integer.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Safely parse a string to BigInt with proper error handling (for transactions)
|
|
94
|
+
*/
|
|
95
|
+
export function parseBigInt(value: string, fieldName: string): bigint {
|
|
96
|
+
return parseBigIntWithCode(value, fieldName, ManifestMCPErrorCode.TX_FAILED);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parse amount string into coin (e.g., "1000umfx" -> { amount: "1000", denom: "umfx" })
|
|
101
|
+
* Supports simple denoms (umfx), IBC denoms (ibc/...), and factory denoms (factory/creator/subdenom)
|
|
102
|
+
*/
|
|
103
|
+
export function parseAmount(amountStr: string): { amount: string; denom: string } {
|
|
104
|
+
// Regex supports alphanumeric denoms with slashes and underscores for IBC/factory denoms
|
|
105
|
+
const match = amountStr.match(/^(\d+)([a-zA-Z][a-zA-Z0-9/_]*)$/);
|
|
106
|
+
if (!match) {
|
|
107
|
+
// Provide specific hints based on common mistakes
|
|
108
|
+
let hint = '';
|
|
109
|
+
if (!amountStr || amountStr.trim() === '') {
|
|
110
|
+
hint = ' Received empty string.';
|
|
111
|
+
} else if (amountStr.includes(' ')) {
|
|
112
|
+
hint = ' Remove the space between number and denom.';
|
|
113
|
+
} else if (amountStr.includes(',')) {
|
|
114
|
+
hint = ' Do not use commas in the number.';
|
|
115
|
+
} else if (/^\d+$/.test(amountStr)) {
|
|
116
|
+
hint = ' Missing denomination (e.g., add "umfx" after the number).';
|
|
117
|
+
} else if (/^[a-zA-Z]/.test(amountStr)) {
|
|
118
|
+
hint = ' Amount must start with a number, not the denomination.';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throw new ManifestMCPError(
|
|
122
|
+
ManifestMCPErrorCode.TX_FAILED,
|
|
123
|
+
`Invalid amount format: "${amountStr}".${hint} Expected format: <number><denom> (e.g., "1000000umfx" or "1000000factory/address/subdenom")`,
|
|
124
|
+
{ receivedValue: amountStr, expectedFormat: '<number><denom>', example: '1000000umfx' }
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return { amount: match[1], denom: match[2] };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Build transaction result from DeliverTxResponse
|
|
132
|
+
*/
|
|
133
|
+
export function buildTxResult(
|
|
134
|
+
module: string,
|
|
135
|
+
subcommand: string,
|
|
136
|
+
result: Awaited<ReturnType<SigningStargateClient['signAndBroadcast']>>,
|
|
137
|
+
waitForConfirmation: boolean
|
|
138
|
+
): CosmosTxResult {
|
|
139
|
+
const txResult: CosmosTxResult = {
|
|
140
|
+
module,
|
|
141
|
+
subcommand,
|
|
142
|
+
transactionHash: result.transactionHash,
|
|
143
|
+
code: result.code,
|
|
144
|
+
height: String(result.height),
|
|
145
|
+
rawLog: result.rawLog || undefined,
|
|
146
|
+
gasUsed: String(result.gasUsed),
|
|
147
|
+
gasWanted: String(result.gasWanted),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (waitForConfirmation) {
|
|
151
|
+
txResult.confirmed = result.code === 0;
|
|
152
|
+
txResult.confirmationHeight = String(result.height);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return txResult;
|
|
156
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { OfflineSigner } from '@cosmjs/proto-signing';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for the Manifest MCP Browser server
|
|
5
|
+
*/
|
|
6
|
+
export interface ManifestMCPConfig {
|
|
7
|
+
/** Chain ID (e.g., "manifest-ledger-testnet") */
|
|
8
|
+
readonly chainId: string;
|
|
9
|
+
/** RPC endpoint URL */
|
|
10
|
+
readonly rpcUrl: string;
|
|
11
|
+
/** Gas price with denomination (e.g., "1.0umfx") */
|
|
12
|
+
readonly gasPrice: string;
|
|
13
|
+
/** Gas adjustment multiplier (default: 1.3) */
|
|
14
|
+
readonly gasAdjustment?: number;
|
|
15
|
+
/** Address prefix (e.g., "manifest") */
|
|
16
|
+
readonly addressPrefix?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Wallet provider interface for different wallet implementations
|
|
21
|
+
*
|
|
22
|
+
* Any wallet that provides an OfflineSigner works (Keplr, Web3Auth, Leap, cosmos-kit, etc.)
|
|
23
|
+
*/
|
|
24
|
+
export interface WalletProvider {
|
|
25
|
+
/** Get the wallet's address */
|
|
26
|
+
getAddress(): Promise<string>;
|
|
27
|
+
/** Get the offline signer for signing transactions */
|
|
28
|
+
getSigner(): Promise<OfflineSigner>;
|
|
29
|
+
/** Optional: Connect to the wallet */
|
|
30
|
+
connect?(): Promise<void>;
|
|
31
|
+
/** Optional: Disconnect from the wallet */
|
|
32
|
+
disconnect?(): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Result from a Cosmos query
|
|
37
|
+
*/
|
|
38
|
+
export interface CosmosQueryResult {
|
|
39
|
+
readonly module: string;
|
|
40
|
+
readonly subcommand: string;
|
|
41
|
+
readonly result: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Result from a Cosmos transaction
|
|
46
|
+
*/
|
|
47
|
+
export interface CosmosTxResult {
|
|
48
|
+
readonly module: string;
|
|
49
|
+
readonly subcommand: string;
|
|
50
|
+
readonly transactionHash: string;
|
|
51
|
+
readonly code: number;
|
|
52
|
+
readonly height: string;
|
|
53
|
+
readonly rawLog?: string;
|
|
54
|
+
confirmed?: boolean;
|
|
55
|
+
confirmationHeight?: string;
|
|
56
|
+
readonly gasUsed?: string;
|
|
57
|
+
readonly gasWanted?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Module information for discovery
|
|
62
|
+
*/
|
|
63
|
+
export interface ModuleInfo {
|
|
64
|
+
readonly name: string;
|
|
65
|
+
readonly description: string;
|
|
66
|
+
readonly args?: string; // Usage hint for arguments
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Available modules listing
|
|
71
|
+
*/
|
|
72
|
+
export interface AvailableModules {
|
|
73
|
+
readonly queryModules: readonly ModuleInfo[];
|
|
74
|
+
readonly txModules: readonly ModuleInfo[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Error codes for ManifestMCPError
|
|
79
|
+
*/
|
|
80
|
+
export enum ManifestMCPErrorCode {
|
|
81
|
+
// Configuration errors
|
|
82
|
+
INVALID_CONFIG = 'INVALID_CONFIG',
|
|
83
|
+
MISSING_CONFIG = 'MISSING_CONFIG',
|
|
84
|
+
|
|
85
|
+
// Wallet errors
|
|
86
|
+
WALLET_NOT_CONNECTED = 'WALLET_NOT_CONNECTED',
|
|
87
|
+
WALLET_CONNECTION_FAILED = 'WALLET_CONNECTION_FAILED',
|
|
88
|
+
KEPLR_NOT_INSTALLED = 'KEPLR_NOT_INSTALLED',
|
|
89
|
+
INVALID_MNEMONIC = 'INVALID_MNEMONIC',
|
|
90
|
+
|
|
91
|
+
// Client errors
|
|
92
|
+
CLIENT_NOT_INITIALIZED = 'CLIENT_NOT_INITIALIZED',
|
|
93
|
+
RPC_CONNECTION_FAILED = 'RPC_CONNECTION_FAILED',
|
|
94
|
+
|
|
95
|
+
// Query errors
|
|
96
|
+
QUERY_FAILED = 'QUERY_FAILED',
|
|
97
|
+
UNSUPPORTED_QUERY = 'UNSUPPORTED_QUERY',
|
|
98
|
+
INVALID_ADDRESS = 'INVALID_ADDRESS',
|
|
99
|
+
|
|
100
|
+
// Transaction errors
|
|
101
|
+
TX_FAILED = 'TX_FAILED',
|
|
102
|
+
TX_SIMULATION_FAILED = 'TX_SIMULATION_FAILED',
|
|
103
|
+
TX_BROADCAST_FAILED = 'TX_BROADCAST_FAILED',
|
|
104
|
+
TX_CONFIRMATION_TIMEOUT = 'TX_CONFIRMATION_TIMEOUT',
|
|
105
|
+
UNSUPPORTED_TX = 'UNSUPPORTED_TX',
|
|
106
|
+
INSUFFICIENT_FUNDS = 'INSUFFICIENT_FUNDS',
|
|
107
|
+
|
|
108
|
+
// Module errors
|
|
109
|
+
UNKNOWN_MODULE = 'UNKNOWN_MODULE',
|
|
110
|
+
UNKNOWN_SUBCOMMAND = 'UNKNOWN_SUBCOMMAND',
|
|
111
|
+
|
|
112
|
+
// General errors
|
|
113
|
+
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Custom error class for Manifest MCP Browser errors
|
|
118
|
+
*/
|
|
119
|
+
export class ManifestMCPError extends Error {
|
|
120
|
+
public readonly code: ManifestMCPErrorCode;
|
|
121
|
+
public readonly details?: Record<string, unknown>;
|
|
122
|
+
|
|
123
|
+
constructor(
|
|
124
|
+
code: ManifestMCPErrorCode,
|
|
125
|
+
message: string,
|
|
126
|
+
details?: Record<string, unknown>
|
|
127
|
+
) {
|
|
128
|
+
super(message);
|
|
129
|
+
this.name = 'ManifestMCPError';
|
|
130
|
+
this.code = code;
|
|
131
|
+
this.details = details;
|
|
132
|
+
|
|
133
|
+
// Ensure proper prototype chain for instanceof checks
|
|
134
|
+
Object.setPrototypeOf(this, ManifestMCPError.prototype);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
toJSON(): Record<string, unknown> {
|
|
138
|
+
return {
|
|
139
|
+
name: this.name,
|
|
140
|
+
code: this.code,
|
|
141
|
+
message: this.message,
|
|
142
|
+
details: this.details,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Account information
|
|
149
|
+
*/
|
|
150
|
+
export interface AccountInfo {
|
|
151
|
+
readonly address: string;
|
|
152
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { DirectSecp256k1HdWallet, OfflineSigner } from '@cosmjs/proto-signing';
|
|
2
|
+
import { WalletProvider, ManifestMCPError, ManifestMCPErrorCode, ManifestMCPConfig } from '../types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mnemonic-based wallet provider for non-browser environments or testing
|
|
6
|
+
*
|
|
7
|
+
* SECURITY NOTE: The mnemonic is stored in memory until disconnect() is called.
|
|
8
|
+
* After disconnect(), the wallet cannot be reconnected - create a new instance instead.
|
|
9
|
+
*/
|
|
10
|
+
export class MnemonicWalletProvider implements WalletProvider {
|
|
11
|
+
private config: ManifestMCPConfig;
|
|
12
|
+
private mnemonic: string | null;
|
|
13
|
+
private wallet: DirectSecp256k1HdWallet | null = null;
|
|
14
|
+
private address: string | null = null;
|
|
15
|
+
private disconnected: boolean = false;
|
|
16
|
+
|
|
17
|
+
constructor(config: ManifestMCPConfig, mnemonic: string) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.mnemonic = mnemonic;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Initialize the wallet from the mnemonic
|
|
24
|
+
*/
|
|
25
|
+
private async initWallet(): Promise<void> {
|
|
26
|
+
if (this.disconnected) {
|
|
27
|
+
throw new ManifestMCPError(
|
|
28
|
+
ManifestMCPErrorCode.WALLET_NOT_CONNECTED,
|
|
29
|
+
'Wallet has been disconnected and cannot be reconnected. Create a new MnemonicWalletProvider instance.'
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (this.wallet) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!this.mnemonic) {
|
|
38
|
+
throw new ManifestMCPError(
|
|
39
|
+
ManifestMCPErrorCode.WALLET_NOT_CONNECTED,
|
|
40
|
+
'Mnemonic has been cleared. Create a new MnemonicWalletProvider instance.'
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const prefix = this.config.addressPrefix ?? 'manifest';
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
this.wallet = await DirectSecp256k1HdWallet.fromMnemonic(this.mnemonic, {
|
|
48
|
+
prefix,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const accounts = await this.wallet.getAccounts();
|
|
52
|
+
if (accounts.length === 0) {
|
|
53
|
+
throw new ManifestMCPError(
|
|
54
|
+
ManifestMCPErrorCode.INVALID_MNEMONIC,
|
|
55
|
+
'No accounts derived from mnemonic'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.address = accounts[0].address;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
throw new ManifestMCPError(
|
|
62
|
+
ManifestMCPErrorCode.INVALID_MNEMONIC,
|
|
63
|
+
`Failed to create wallet from mnemonic: ${error instanceof Error ? error.message : String(error)}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Connect (initialize) the wallet
|
|
70
|
+
*/
|
|
71
|
+
async connect(): Promise<void> {
|
|
72
|
+
await this.initWallet();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Disconnect and securely clear all sensitive data
|
|
77
|
+
*
|
|
78
|
+
* IMPORTANT: After calling disconnect(), this wallet instance cannot be reused.
|
|
79
|
+
* Create a new MnemonicWalletProvider instance if you need to reconnect.
|
|
80
|
+
*/
|
|
81
|
+
async disconnect(): Promise<void> {
|
|
82
|
+
// Clear the mnemonic by overwriting with empty string then nullifying
|
|
83
|
+
// Note: JavaScript strings are immutable, so we can't truly zero the memory,
|
|
84
|
+
// but we can remove all references to allow garbage collection
|
|
85
|
+
this.mnemonic = null;
|
|
86
|
+
this.wallet = null;
|
|
87
|
+
this.address = null;
|
|
88
|
+
this.disconnected = true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get the wallet's address
|
|
93
|
+
*/
|
|
94
|
+
async getAddress(): Promise<string> {
|
|
95
|
+
await this.initWallet();
|
|
96
|
+
|
|
97
|
+
if (!this.address) {
|
|
98
|
+
throw new ManifestMCPError(
|
|
99
|
+
ManifestMCPErrorCode.WALLET_NOT_CONNECTED,
|
|
100
|
+
'Wallet failed to initialize'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return this.address;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get the offline signer for signing transactions
|
|
109
|
+
*/
|
|
110
|
+
async getSigner(): Promise<OfflineSigner> {
|
|
111
|
+
await this.initWallet();
|
|
112
|
+
|
|
113
|
+
if (!this.wallet) {
|
|
114
|
+
throw new ManifestMCPError(
|
|
115
|
+
ManifestMCPErrorCode.WALLET_NOT_CONNECTED,
|
|
116
|
+
'Wallet failed to initialize'
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return this.wallet;
|
|
121
|
+
}
|
|
122
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ES2020",
|
|
5
|
+
"lib": ["ES2020", "DOM"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noUnusedLocals": true,
|
|
10
|
+
"noUnusedParameters": true,
|
|
11
|
+
"noFallthroughCasesInSwitch": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"moduleResolution": "node",
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"declaration": true,
|
|
18
|
+
"declarationMap": true,
|
|
19
|
+
"sourceMap": true
|
|
20
|
+
},
|
|
21
|
+
"include": ["src/**/*"],
|
|
22
|
+
"exclude": ["node_modules", "dist"]
|
|
23
|
+
}
|