Skip to content

Commit 8da1935

Browse files
committed
feat: minimal addressTools for query param parsing
1 parent 30f719f commit 8da1935

File tree

2 files changed

+135
-0
lines changed

2 files changed

+135
-0
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { makeError, q } from '@endo/errors';
2+
3+
/**
4+
* Very minimal 'URL query string'-like parser that handles:
5+
* - Query string delimiter (?)
6+
* - Key-value separator (=)
7+
* - Query parameter separator (&)
8+
*
9+
* Does not handle:
10+
* - Subpaths (`agoric1bech32addr/opt/account?k=v`)
11+
* - URI encoding/decoding (`%20` -> ` `)
12+
* - Multiple question marks (foo?bar=1?baz=2)
13+
* - Empty parameters (foo=)
14+
* - Array parameters (`foo?k=v1&k=v2` -> k: [v1, v2])
15+
* - Parameters without values (foo&bar=2)
16+
*/
17+
export const addressTools = {
18+
/**
19+
* @param {string} address
20+
* @returns {boolean}
21+
*/
22+
hasQueryParams: address => {
23+
return address.includes('?');
24+
},
25+
/**
26+
* @param {string} address
27+
* @returns {{ address: string, params: Record<string, string>}}
28+
*/
29+
getQueryParams: address => {
30+
const parts = address.split('?');
31+
if (parts.length === 0 || parts.length > 2) {
32+
throw makeError(
33+
`Invalid input. Must be of the form 'address?params': ${q(address)}`,
34+
);
35+
}
36+
const result = {
37+
address: parts[0],
38+
params: {},
39+
};
40+
41+
// no parameters, return early
42+
if (parts.length === 1) {
43+
return result;
44+
}
45+
46+
const paramPairs = parts[1].split('&');
47+
for (const pair of paramPairs) {
48+
const [key, value] = pair.split('=');
49+
if (!key || !value) {
50+
throw makeError(`Invalid parameter format in pair: ${q(pair)}`);
51+
}
52+
result.params[key] = value;
53+
}
54+
55+
return result;
56+
},
57+
};
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js';
2+
3+
import { addressTools } from '../../src/utils/address.js';
4+
5+
const FIXTURES = {
6+
AGORIC_WITH_DYDX:
7+
'agoric1bech32addr?EUD=dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men',
8+
AGORIC_WITH_OSMO:
9+
'agoric1bech32addr?EUD=osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men',
10+
AGORIC_WITH_MULTIPLE:
11+
'agoric1bech32addr?EUD=osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men&CID=dydx-mainnet-1',
12+
AGORIC_NO_PARAMS: 'agoric1bech32addr',
13+
INVALID_MULTIPLE_QUESTION: 'agoric1bech32addr?param1=value1?param2=value2',
14+
INVALID_PARAM_FORMAT: 'agoric1bech32addr?invalidparam',
15+
} as const;
16+
17+
// hasQueryParams tests
18+
test('hasQueryParams: returns true when address has parameters', t => {
19+
t.true(addressTools.hasQueryParams(FIXTURES.AGORIC_WITH_DYDX));
20+
t.true(addressTools.hasQueryParams(FIXTURES.AGORIC_WITH_OSMO));
21+
t.true(addressTools.hasQueryParams(FIXTURES.AGORIC_WITH_MULTIPLE));
22+
});
23+
24+
test('hasQueryParams: returns false when address has no parameters', t => {
25+
t.false(addressTools.hasQueryParams(FIXTURES.AGORIC_NO_PARAMS));
26+
});
27+
28+
test('hasQueryParams: returns true for invalid parameter formats (only checks for ?)', t => {
29+
t.true(addressTools.hasQueryParams(FIXTURES.INVALID_MULTIPLE_QUESTION));
30+
t.true(addressTools.hasQueryParams(FIXTURES.INVALID_PARAM_FORMAT));
31+
});
32+
33+
// getQueryParams tests - positive cases
34+
test('getQueryParams: correctly parses address with single EUD parameter', t => {
35+
const result = addressTools.getQueryParams(FIXTURES.AGORIC_WITH_DYDX);
36+
t.deepEqual(result, {
37+
address: 'agoric1bech32addr',
38+
params: {
39+
EUD: 'dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men',
40+
},
41+
});
42+
});
43+
44+
test('getQueryParams: correctly parses address with multiple parameters', t => {
45+
const result = addressTools.getQueryParams(FIXTURES.AGORIC_WITH_MULTIPLE);
46+
t.deepEqual(result, {
47+
address: 'agoric1bech32addr',
48+
params: {
49+
EUD: 'osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men',
50+
CID: 'dydx-mainnet-1',
51+
},
52+
});
53+
});
54+
55+
test('getQueryParams: correctly handles address with no parameters', t => {
56+
const result = addressTools.getQueryParams(FIXTURES.AGORIC_NO_PARAMS);
57+
t.deepEqual(result, {
58+
address: 'agoric1bech32addr',
59+
params: {},
60+
});
61+
});
62+
63+
// getQueryParams tests - negative cases
64+
test('getQueryParams: throws error for multiple question marks', t => {
65+
t.throws(
66+
() => addressTools.getQueryParams(FIXTURES.INVALID_MULTIPLE_QUESTION),
67+
{
68+
message:
69+
'Invalid input. Must be of the form \'address?params\': "agoric1bech32addr?param1=value1?param2=value2"',
70+
},
71+
);
72+
});
73+
74+
test('getQueryParams: throws error for invalid parameter format', t => {
75+
t.throws(() => addressTools.getQueryParams(FIXTURES.INVALID_PARAM_FORMAT), {
76+
message: 'Invalid parameter format in pair: "invalidparam"',
77+
});
78+
});

0 commit comments

Comments
 (0)