@wikialisalabs/ans-resolver 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,33 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wikialisalabs (https://github.com/wikialisalabs)
4
+ Author & Founder: Nguyen Thi Thu Huong (wikialisa)
5
+ Technical Contact: wiki@axvn.vn
6
+ Organization: wikialisalabs
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+
26
+ ---
27
+
28
+ ### Tóm tắt ý nghĩa của giấy phép MIT đối với tổ chức wikialisalabs:
29
+
30
+ * **Chuyển giao toàn diện bản quyền:** Quyền sở hữu trí tuệ và thương hiệu mã nguồn thuộc về tổ chức **wikialisalabs** do tác giả Nguyễn Thị Thu Hương sáng lập.
31
+ * **Tự do thương mại hóa tối đa:** Bất kỳ cá nhân hoặc tổ chức nào cũng có thể sử dụng, tích hợp và thương mại hóa mã nguồn của hệ sinh thái mà không bị rào cản BSL hay bản quyền đóng.
32
+ * **Tự do tùy biến & Đóng góp:** Cộng đồng được quyền fork, phát triển module mở rộng, tối ưu hóa các thành phần kiến trúc và đóng góp lại cho `wikialisalabs`.
33
+ * **Miễn trừ trách nhiệm pháp lý:** Tổ chức `wikialisalabs` và tác giả được miễn trừ toàn bộ trách nhiệm pháp lý đối với bất kỳ rủi ro vận hành hoặc tổn thất tài chính nào phát sinh từ việc sử dụng phần mềm.
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "@wikialisalabs/ans-resolver",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "ANS Domain Resolver SDK — resolve .axq/.vpx/.sqx/.kpx/.vrq names on-chain via ANSRegistry contract",
5
+ "main": "src/index.js",
6
+ "types": "src/index.d.ts",
7
+ "license": "MIT",
8
+ "dependencies": {
9
+ "viem": "^2.17.0"
10
+ },
11
+ "devDependencies": {
12
+ "jest": "^30.5.2"
13
+ },
14
+ "jest": {
15
+ "testEnvironment": "node",
16
+ "testMatch": [
17
+ "**/src/__tests__/**/*.test.js"
18
+ ],
19
+ "collectCoverageFrom": [
20
+ "src/**/*.js",
21
+ "!src/__tests__/**"
22
+ ],
23
+ "coverageThreshold": {
24
+ "global": {
25
+ "lines": 60
26
+ }
27
+ }
28
+ },
29
+ "author": "wikialisa (Nguyen Thi Thu Huong) <wiki@axvn.vn>",
30
+ "publishConfig": {
31
+ "access": "public",
32
+ "registry": "https://registry.npmjs.org/"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/wikialisalabs/ans-resolver.git"
37
+ },
38
+ "scripts": {
39
+ "build": "echo 'ANS Resolver: JS-only, no build step needed'",
40
+ "typecheck": "echo 'ANS Resolver: no TypeScript yet — Phase 5 migration'",
41
+ "test": "jest --forceExit",
42
+ "test:ci": "jest --forceExit --ci --coverage",
43
+ "clean": "rm -rf dist coverage"
44
+ }
6
45
  }
@@ -0,0 +1,155 @@
1
+ 'use strict';
2
+ /**
3
+ * @axioledger/ans-resolver — unit tests
4
+ * All on-chain calls are mocked via jest — no real RPC required.
5
+ */
6
+
7
+ jest.mock('viem', () => {
8
+ const mockReadContract = jest.fn();
9
+ const mockCreatePublicClient = jest.fn(() => ({ readContract: mockReadContract }));
10
+ return {
11
+ createPublicClient: mockCreatePublicClient,
12
+ http: jest.fn((url) => ({ url })),
13
+ keccak256: jest.fn((data) => `0x${'a'.repeat(64)}`),
14
+ encodePacked: jest.fn(() => `0x${'b'.repeat(64)}`),
15
+ isAddress: jest.fn((addr) => /^0x[0-9a-fA-F]{40}$/.test(addr)),
16
+ __readContract: mockReadContract,
17
+ };
18
+ });
19
+
20
+ jest.mock('viem/chains', () => ({
21
+ sepolia: { id: 11155111, name: 'Sepolia' },
22
+ mainnet: { id: 1, name: 'Ethereum' },
23
+ }));
24
+
25
+ const REGISTRY = '0x' + 'C'.repeat(40);
26
+ const { ANSResolver, SUPPORTED_TLDS, parseName, computeNameHash } = require('../index');
27
+
28
+ function getReadMock() {
29
+ return require('viem').__readContract;
30
+ }
31
+
32
+ describe('parseName', () => {
33
+ it('parses "alice.axq"', () => {
34
+ const r = parseName('alice.axq');
35
+ expect(r).toEqual({ label: 'alice', tld: 'axq' });
36
+ });
37
+
38
+ it('parses subdomain "bob.vpx"', () => {
39
+ const r = parseName('bob.vpx');
40
+ expect(r.tld).toBe('vpx');
41
+ });
42
+
43
+ it('throws on unsupported TLD', () => {
44
+ expect(() => parseName('alice.eth')).toThrow('Unsupported TLD');
45
+ });
46
+
47
+ it('throws on no TLD', () => {
48
+ expect(() => parseName('alice')).toThrow('Invalid ANS name');
49
+ });
50
+ });
51
+
52
+ describe('SUPPORTED_TLDS', () => {
53
+ it('contains all 5 pillars', () => {
54
+ expect(SUPPORTED_TLDS).toEqual(expect.arrayContaining(['axq','vpx','sqx','kpx','vrq']));
55
+ expect(SUPPORTED_TLDS).toHaveLength(5);
56
+ });
57
+ });
58
+
59
+ describe('ANSResolver constructor', () => {
60
+ it('throws without rpcUrl', () => {
61
+ expect(() => new ANSResolver({ registryAddress: REGISTRY })).toThrow('rpcUrl');
62
+ });
63
+
64
+ it('throws without registryAddress', () => {
65
+ expect(() => new ANSResolver({ rpcUrl: 'http://x' })).toThrow('registryAddress');
66
+ });
67
+
68
+ it('throws on invalid address', () => {
69
+ expect(() => new ANSResolver({ rpcUrl: 'http://x', registryAddress: 'notanaddress' }))
70
+ .toThrow('invalid registryAddress');
71
+ });
72
+
73
+ it('constructs successfully with valid config', () => {
74
+ const r = new ANSResolver({ rpcUrl: 'http://localhost:8545', registryAddress: REGISTRY });
75
+ expect(r).toBeInstanceOf(ANSResolver);
76
+ expect(r.registryAddress).toBe(REGISTRY);
77
+ });
78
+ });
79
+
80
+ describe('ANSResolver.resolve', () => {
81
+ let resolver;
82
+ beforeEach(() => {
83
+ resolver = new ANSResolver({ rpcUrl: 'http://localhost:8545', registryAddress: REGISTRY });
84
+ getReadMock().mockReset();
85
+ });
86
+
87
+ it('returns owner address for a registered name', async () => {
88
+ const expected = '0x' + 'A'.repeat(40);
89
+ getReadMock().mockResolvedValue(expected);
90
+
91
+ const result = await resolver.resolve('alice', 'axq');
92
+ expect(result).toBe(expected);
93
+ expect(getReadMock()).toHaveBeenCalledWith(expect.objectContaining({
94
+ functionName: 'resolve',
95
+ args: ['alice', 'axq'],
96
+ }));
97
+ });
98
+
99
+ it('resolveToAddress parses and resolves "alice.axq"', async () => {
100
+ const expected = '0x' + 'B'.repeat(40);
101
+ getReadMock().mockResolvedValue(expected);
102
+
103
+ const result = await resolver.resolveToAddress('alice.axq');
104
+ expect(result).toBe(expected);
105
+ });
106
+ });
107
+
108
+ describe('ANSResolver.getRecord', () => {
109
+ let resolver;
110
+ beforeEach(() => {
111
+ resolver = new ANSResolver({ rpcUrl: 'http://localhost:8545', registryAddress: REGISTRY });
112
+ getReadMock().mockReset();
113
+ });
114
+
115
+ it('returns structured record', async () => {
116
+ const owner = '0x' + 'D'.repeat(40);
117
+ const resolver_= '0x' + '0'.repeat(40);
118
+ const expiry = BigInt(Math.floor(Date.now() / 1000) + 86400);
119
+ getReadMock().mockResolvedValue([owner, resolver_, expiry, false]);
120
+
121
+ const rec = await resolver.getRecord('alice.axq');
122
+ expect(rec.owner).toBe(owner);
123
+ expect(rec.expiry).toBe(expiry);
124
+ expect(rec.locked).toBe(false);
125
+ });
126
+ });
127
+
128
+ describe('ANSResolver.isRegistered', () => {
129
+ let resolver;
130
+ beforeEach(() => {
131
+ resolver = new ANSResolver({ rpcUrl: 'http://localhost:8545', registryAddress: REGISTRY });
132
+ getReadMock().mockReset();
133
+ });
134
+
135
+ it('returns true for active name', async () => {
136
+ const owner = '0x' + 'E'.repeat(40);
137
+ const expiry = BigInt(Math.floor(Date.now() / 1000) + 86400);
138
+ getReadMock().mockResolvedValue([owner, '0x' + '0'.repeat(40), expiry, false]);
139
+
140
+ expect(await resolver.isRegistered('alice.axq')).toBe(true);
141
+ });
142
+
143
+ it('returns false for expired name', async () => {
144
+ const owner = '0x' + 'F'.repeat(40);
145
+ const expiry = BigInt(Math.floor(Date.now() / 1000) - 100); // expired
146
+ getReadMock().mockResolvedValue([owner, '0x' + '0'.repeat(40), expiry, false]);
147
+
148
+ expect(await resolver.isRegistered('alice.axq')).toBe(false);
149
+ });
150
+
151
+ it('returns false when readContract throws', async () => {
152
+ getReadMock().mockRejectedValue(new Error('not registered'));
153
+ expect(await resolver.isRegistered('unknown.axq')).toBe(false);
154
+ });
155
+ });
package/src/index.js ADDED
@@ -0,0 +1,240 @@
1
+ 'use strict';
2
+ /**
3
+ * @axioledger/ans-resolver — AXIOLEDGER Name Service (ANS) TypeScript client
4
+ *
5
+ * Resolves .axq / .vpx / .sqx / .kpx / .vrq names to addresses on-chain.
6
+ * Reads from the deployed ANSRegistry smart contract via viem.
7
+ *
8
+ * @example
9
+ * import { ANSResolver } from '@axioledger/ans-resolver';
10
+ *
11
+ * const resolver = new ANSResolver({
12
+ * rpcUrl: 'https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY',
13
+ * registryAddress: '0x...',
14
+ * });
15
+ *
16
+ * const owner = await resolver.resolve('alice', 'axq');
17
+ * const addr = await resolver.resolveToAddress('alice.axq');
18
+ */
19
+
20
+ const { createPublicClient, http, keccak256, encodePacked, isAddress } = require('viem');
21
+ const { sepolia, mainnet } = require('viem/chains');
22
+
23
+ // ── ABI (minimal — only functions we call) ────────────────────────────────────
24
+ const ANS_REGISTRY_ABI = [
25
+ {
26
+ name: 'resolve',
27
+ type: 'function',
28
+ stateMutability: 'view',
29
+ inputs: [
30
+ { name: 'label', type: 'string' },
31
+ { name: 'tld', type: 'string' },
32
+ ],
33
+ outputs: [{ name: '', type: 'address' }],
34
+ },
35
+ {
36
+ name: 'getResolver',
37
+ type: 'function',
38
+ stateMutability: 'view',
39
+ inputs: [
40
+ { name: 'label', type: 'string' },
41
+ { name: 'tld', type: 'string' },
42
+ ],
43
+ outputs: [{ name: '', type: 'address' }],
44
+ },
45
+ {
46
+ name: 'records',
47
+ type: 'function',
48
+ stateMutability: 'view',
49
+ inputs: [{ name: 'nameHash', type: 'bytes32' }],
50
+ outputs: [
51
+ { name: 'owner', type: 'address' },
52
+ { name: 'resolver', type: 'address' },
53
+ { name: 'expiry', type: 'uint64' },
54
+ { name: 'locked', type: 'bool' },
55
+ ],
56
+ },
57
+ {
58
+ name: 'tldFees',
59
+ type: 'function',
60
+ stateMutability: 'view',
61
+ inputs: [{ name: 'tldHash', type: 'bytes32' }],
62
+ outputs: [{ name: '', type: 'uint256' }],
63
+ },
64
+ {
65
+ name: 'supportedTlds',
66
+ type: 'function',
67
+ stateMutability: 'view',
68
+ inputs: [{ name: 'tldHash', type: 'bytes32' }],
69
+ outputs: [{ name: '', type: 'bool' }],
70
+ },
71
+ ];
72
+
73
+ // ── Supported TLDs ────────────────────────────────────────────────────────────
74
+
75
+ const SUPPORTED_TLDS = ['axq', 'vpx', 'sqx', 'kpx', 'vrq'];
76
+
77
+ const CHAIN_MAP = {
78
+ mainnet: { chain: mainnet, id: 1 },
79
+ sepolia: { chain: sepolia, id: 11155111 },
80
+ };
81
+
82
+ // ── Helper: compute nameHash matching ANSRegistry._nameHash() ─────────────────
83
+ function computeNameHash(label, tld) {
84
+ const labelHash = keccak256(new TextEncoder().encode(label));
85
+ const tldHash = keccak256(new TextEncoder().encode(tld));
86
+ return keccak256(encodePacked(['bytes32', 'bytes32'], [labelHash, tldHash]));
87
+ }
88
+
89
+ // ── Parse "alice.axq" → { label: "alice", tld: "axq" } ───────────────────────
90
+ function parseName(name) {
91
+ const parts = name.trim().toLowerCase().split('.');
92
+ if (parts.length < 2) throw new Error(`Invalid ANS name: "${name}" — expected "label.tld"`);
93
+ const tld = parts[parts.length - 1];
94
+ const label = parts.slice(0, -1).join('.');
95
+ if (!SUPPORTED_TLDS.includes(tld)) {
96
+ throw new Error(`Unsupported TLD ".${tld}" — supported: ${SUPPORTED_TLDS.map(t => `.${t}`).join(', ')}`);
97
+ }
98
+ return { label, tld };
99
+ }
100
+
101
+ // ── ANSResolver class ─────────────────────────────────────────────────────────
102
+
103
+ class ANSResolver {
104
+ /**
105
+ * @param {object} config
106
+ * @param {string} config.rpcUrl — EVM JSON-RPC endpoint
107
+ * @param {string} config.registryAddress — Deployed ANSRegistry contract address
108
+ * @param {'mainnet'|'sepolia'} [config.network='sepolia']
109
+ */
110
+ constructor(config) {
111
+ if (!config.rpcUrl) throw new Error('ANSResolver: rpcUrl is required');
112
+ if (!config.registryAddress) throw new Error('ANSResolver: registryAddress is required');
113
+ if (!isAddress(config.registryAddress)) {
114
+ throw new Error(`ANSResolver: invalid registryAddress "${config.registryAddress}"`);
115
+ }
116
+
117
+ const network = config.network || 'sepolia';
118
+ const chainInfo = CHAIN_MAP[network];
119
+ if (!chainInfo) throw new Error(`ANSResolver: unknown network "${network}"`);
120
+
121
+ this.registryAddress = config.registryAddress;
122
+ this.client = createPublicClient({
123
+ chain: chainInfo.chain,
124
+ transport: http(config.rpcUrl),
125
+ });
126
+ }
127
+
128
+ // ── Public API ─────────────────────────────────────────────────────────────
129
+
130
+ /**
131
+ * Resolve "alice.axq" → owner address.
132
+ * Throws if name is expired or not registered.
133
+ * @param {string} name e.g. "alice.axq"
134
+ * @returns {Promise<string>} checksummed Ethereum address
135
+ */
136
+ async resolveToAddress(name) {
137
+ const { label, tld } = parseName(name);
138
+ return this.resolve(label, tld);
139
+ }
140
+
141
+ /**
142
+ * Resolve label + tld separately → owner address.
143
+ * @param {string} label e.g. "alice"
144
+ * @param {string} tld e.g. "axq"
145
+ * @returns {Promise<string>} checksummed Ethereum address
146
+ */
147
+ async resolve(label, tld) {
148
+ const address = await this.client.readContract({
149
+ address: this.registryAddress,
150
+ abi: ANS_REGISTRY_ABI,
151
+ functionName: 'resolve',
152
+ args: [label.toLowerCase(), tld.toLowerCase()],
153
+ });
154
+ return address;
155
+ }
156
+
157
+ /**
158
+ * Get resolver contract address for extended records.
159
+ * @param {string} name e.g. "alice.axq"
160
+ * @returns {Promise<string>} resolver contract address (may be zero address)
161
+ */
162
+ async getResolver(name) {
163
+ const { label, tld } = parseName(name);
164
+ return this.client.readContract({
165
+ address: this.registryAddress,
166
+ abi: ANS_REGISTRY_ABI,
167
+ functionName: 'getResolver',
168
+ args: [label.toLowerCase(), tld.toLowerCase()],
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Fetch full record details (owner, resolver, expiry, locked).
174
+ * @param {string} name e.g. "alice.axq"
175
+ * @returns {Promise<{owner: string, resolver: string, expiry: bigint, locked: boolean}>}
176
+ */
177
+ async getRecord(name) {
178
+ const { label, tld } = parseName(name);
179
+ const nameHash = computeNameHash(label.toLowerCase(), tld.toLowerCase());
180
+
181
+ const [owner, resolver, expiry, locked] = await this.client.readContract({
182
+ address: this.registryAddress,
183
+ abi: ANS_REGISTRY_ABI,
184
+ functionName: 'records',
185
+ args: [nameHash],
186
+ });
187
+
188
+ return { owner, resolver, expiry, locked };
189
+ }
190
+
191
+ /**
192
+ * Check whether a name is registered and not expired.
193
+ * @param {string} name e.g. "alice.axq"
194
+ * @returns {Promise<boolean>}
195
+ */
196
+ async isRegistered(name) {
197
+ try {
198
+ const record = await this.getRecord(name);
199
+ const now = BigInt(Math.floor(Date.now() / 1000));
200
+ return record.owner !== '0x0000000000000000000000000000000000000000'
201
+ && record.expiry > now;
202
+ } catch {
203
+ return false;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Get registration fee for a TLD (in wei).
209
+ * @param {string} tld e.g. "axq"
210
+ * @returns {Promise<bigint>}
211
+ */
212
+ async getRegistrationFee(tld) {
213
+ const tldHash = keccak256(new TextEncoder().encode(tld.toLowerCase()));
214
+ return this.client.readContract({
215
+ address: this.registryAddress,
216
+ abi: ANS_REGISTRY_ABI,
217
+ functionName: 'tldFees',
218
+ args: [tldHash],
219
+ });
220
+ }
221
+
222
+ /**
223
+ * Check whether a TLD is supported by the registry.
224
+ * @param {string} tld e.g. "axq"
225
+ * @returns {Promise<boolean>}
226
+ */
227
+ async isTldSupported(tld) {
228
+ const tldHash = keccak256(new TextEncoder().encode(tld.toLowerCase()));
229
+ return this.client.readContract({
230
+ address: this.registryAddress,
231
+ abi: ANS_REGISTRY_ABI,
232
+ functionName: 'supportedTlds',
233
+ args: [tldHash],
234
+ });
235
+ }
236
+ }
237
+
238
+ // ── Exports ───────────────────────────────────────────────────────────────────
239
+
240
+ module.exports = { ANSResolver, SUPPORTED_TLDS, computeNameHash, parseName };
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.