@hoodag/sdk 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.
Files changed (3) hide show
  1. package/README.md +67 -0
  2. package/hood.js +107 -0
  3. package/package.json +31 -0
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # @hoodag/sdk
2
+
3
+ Resolve **.hood** names on [Robinhood Chain](https://hood.ag) (chain id `4663`). ENS-standard,
4
+ so it behaves like ENS resolution — but every read goes through a single contract, the
5
+ **UniversalResolver**, which makes integration a one-liner.
6
+
7
+ Zero dependencies (uses global `fetch`, works in the browser and Node 18+). Optional
8
+ [viem](https://viem.sh) helpers are exported if your app already uses viem.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @hoodag/sdk
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```js
19
+ import { resolveHood, reverseHood, resolveTextHood } from '@hoodag/sdk';
20
+
21
+ // Forward: name -> address
22
+ const addr = await resolveHood('robin.hood');
23
+ // '0x45aa14…' (or null if unset)
24
+
25
+ // Reverse: address -> primary name (verified = forward record points back)
26
+ const { name, verified } = await reverseHood(addr);
27
+ // { name: 'robin.hood', verified: true }
28
+
29
+ // Text records
30
+ const twitter = await resolveTextHood('robin.hood', 'com.twitter');
31
+ ```
32
+
33
+ Every function accepts an optional `rpcUrl` as the last argument if you run your own node:
34
+
35
+ ```js
36
+ await resolveHood('robin.hood', 'https://my-node.example/rpc');
37
+ ```
38
+
39
+ ## viem helpers
40
+
41
+ ```js
42
+ import { createPublicClient, http } from 'viem';
43
+ import { HOOD, UNIVERSAL_RESOLVER_ABI } from '@hoodag/sdk';
44
+
45
+ const client = createPublicClient({ transport: http(HOOD.rpcUrl) });
46
+ const addr = await client.readContract({
47
+ address: HOOD.contracts.universalResolver,
48
+ abi: UNIVERSAL_RESOLVER_ABI,
49
+ functionName: 'resolve',
50
+ args: ['robin.hood'],
51
+ });
52
+ ```
53
+
54
+ ## Contracts (Robinhood Chain, id 4663)
55
+
56
+ | Contract | Address |
57
+ | --- | --- |
58
+ | Registry (immutable) | `0x3E717dc89AAF4605b607324329EEeB0a8B3C8A1c` |
59
+ | UniversalResolver | `0xa9ed3C73F522875F18DAa631429125da893D8E53` |
60
+ | Registrar (.hood NFT) | `0x9a122C54e15B6287c164b90833bcDA6E16520D50` |
61
+ | Public Resolver | `0x072C7a66a6D52B23274470b86096AFCBF816CA0B` |
62
+ | Registrar Controller | `0x7900b2B8Cc3f0616B18C68AdA7E115A9022Ca04a` |
63
+ | Reverse Registrar | `0x4be6D0379f56404B563afAe17c6b095b1dfcf479` |
64
+
65
+ ## License
66
+
67
+ MIT
package/hood.js ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * .hood name service SDK — resolve .hood names on Robinhood Chain (4663).
3
+ *
4
+ * Drop-in for any app. Two flavors:
5
+ * 1) Zero-dependency (uses global fetch) — resolveHood / reverseHood / resolveTextHood
6
+ * 2) viem helpers (if you already use viem) — at the bottom
7
+ *
8
+ * All calls hit ONE contract (the HoodUniversalResolver), so integration is a single read.
9
+ */
10
+
11
+ export const HOOD = {
12
+ chainId: 4663,
13
+ rpcUrl: 'https://rpc.mainnet.chain.robinhood.com',
14
+ contracts: {
15
+ registry: '0x3E717dc89AAF4605b607324329EEeB0a8B3C8A1c', // immutable trust anchor
16
+ universalResolver: '0xa9ed3C73F522875F18DAa631429125da893D8E53', // <-- call this
17
+ registrar: '0x9a122C54e15B6287c164b90833bcDA6E16520D50',
18
+ resolver: '0x072C7a66a6D52B23274470b86096AFCBF816CA0B',
19
+ controller: '0x7900b2B8Cc3f0616B18C68AdA7E115A9022Ca04a',
20
+ },
21
+ };
22
+
23
+ // ---------- zero-dependency implementation (works in browser & node 18+) ----------
24
+ function keccakSelector(sig) {
25
+ // precomputed 4-byte selectors (verified against the deployed UniversalResolver):
26
+ const map = {
27
+ 'resolve(string)': '0x461a4478',
28
+ 'reverse(address)': '0xe30bd740',
29
+ 'resolveText(string,string)': '0xcb7cef3c',
30
+ };
31
+ return map[sig];
32
+ }
33
+
34
+ function encStringArg(str, extra = '') {
35
+ // ABI-encode a single dynamic string (+ optional already-encoded trailing words)
36
+ const bytes = new TextEncoder().encode(str);
37
+ const hex = [...bytes].map(b => b.toString(16).padStart(2, '0')).join('');
38
+ const len = bytes.length.toString(16).padStart(64, '0');
39
+ const padded = hex.padEnd(Math.ceil(hex.length / 64) * 64, '0');
40
+ // head: offset to string = 0x20 (or 0x40 if there's a second static/dynamic arg — handled by caller)
41
+ return { len, padded, hexLen: bytes.length };
42
+ }
43
+
44
+ async function ethCall(to, data, rpcUrl = HOOD.rpcUrl) {
45
+ const res = await fetch(rpcUrl, {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json' },
48
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to, data }, 'latest'] }),
49
+ });
50
+ const j = await res.json();
51
+ if (j.error) throw new Error(j.error.message);
52
+ return j.result;
53
+ }
54
+
55
+ /** resolveHood("robin.hood") -> "0x..." (or null if unset) */
56
+ export async function resolveHood(name, rpcUrl = HOOD.rpcUrl) {
57
+ const { len, padded } = encStringArg(name);
58
+ const data = keccakSelector('resolve(string)') +
59
+ '0000000000000000000000000000000000000000000000000000000000000020' + len + padded;
60
+ const out = await ethCall(HOOD.contracts.universalResolver, data, rpcUrl);
61
+ const addr = '0x' + out.slice(-40);
62
+ return /^0x0{40}$/.test(addr) ? null : addr;
63
+ }
64
+
65
+ /** reverseHood("0x...") -> { name, verified } */
66
+ export async function reverseHood(address, rpcUrl = HOOD.rpcUrl) {
67
+ const data = keccakSelector('reverse(address)') + address.toLowerCase().replace('0x', '').padStart(64, '0');
68
+ const out = await ethCall(HOOD.contracts.universalResolver, data, rpcUrl);
69
+ const b = out.slice(2);
70
+ const strOffset = parseInt(b.slice(0, 64), 16) * 2;
71
+ const verified = parseInt(b.slice(64, 128), 16) === 1;
72
+ const strLen = parseInt(b.slice(strOffset, strOffset + 64), 16) * 2;
73
+ const strHex = b.slice(strOffset + 64, strOffset + 64 + strLen);
74
+ const name = new TextDecoder().decode(Uint8Array.from(strHex.match(/../g)?.map(h => parseInt(h, 16)) ?? []));
75
+ return { name: name || null, verified };
76
+ }
77
+
78
+ /** resolveTextHood("robin.hood", "com.twitter") -> "..." */
79
+ export async function resolveTextHood(name, key, rpcUrl = HOOD.rpcUrl) {
80
+ const n = encStringArg(name), k = encStringArg(key);
81
+ // two dynamic args: head = [off1=0x40, off2], then arg1, then arg2
82
+ const head = '0000000000000000000000000000000000000000000000000000000000000040';
83
+ const arg1 = n.len + n.padded;
84
+ const off2 = (0x40 + arg1.length / 2).toString(16).padStart(64, '0');
85
+ const arg2 = k.len + k.padded;
86
+ const data = keccakSelector('resolveText(string,string)') + head + off2 + arg1 + arg2;
87
+ const out = await ethCall(HOOD.contracts.universalResolver, data, rpcUrl);
88
+ const b = out.slice(2);
89
+ const strLen = parseInt(b.slice(64, 128), 16) * 2;
90
+ const strHex = b.slice(128, 128 + strLen);
91
+ return new TextDecoder().decode(Uint8Array.from(strHex.match(/../g)?.map(h => parseInt(h, 16)) ?? []));
92
+ }
93
+
94
+ // ---------- viem helpers (optional; if your app already uses viem) ----------
95
+ export const UNIVERSAL_RESOLVER_ABI = [
96
+ { type: 'function', name: 'resolve', stateMutability: 'view', inputs: [{ name: 'name', type: 'string' }], outputs: [{ type: 'address' }] },
97
+ { type: 'function', name: 'reverse', stateMutability: 'view', inputs: [{ name: 'a', type: 'address' }], outputs: [{ name: 'name', type: 'string' }, { name: 'verified', type: 'bool' }] },
98
+ { type: 'function', name: 'resolveText', stateMutability: 'view', inputs: [{ name: 'name', type: 'string' }, { name: 'key', type: 'string' }], outputs: [{ type: 'string' }] },
99
+ ];
100
+
101
+ /* viem usage:
102
+ import { createPublicClient, http } from 'viem';
103
+ import { HOOD, UNIVERSAL_RESOLVER_ABI } from './hood.js';
104
+ const client = createPublicClient({ transport: http(HOOD.rpcUrl) });
105
+ const addr = await client.readContract({ address: HOOD.contracts.universalResolver, abi: UNIVERSAL_RESOLVER_ABI, functionName: 'resolve', args: ['robin.hood'] });
106
+ const [name, verified] = await client.readContract({ address: HOOD.contracts.universalResolver, abi: UNIVERSAL_RESOLVER_ABI, functionName: 'reverse', args: [addr] });
107
+ */
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@hoodag/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Resolve .hood names on Robinhood Chain (ENS-standard) — one call, zero dependencies, optional viem helpers.",
5
+ "type": "module",
6
+ "main": "./hood.js",
7
+ "module": "./hood.js",
8
+ "exports": {
9
+ ".": "./hood.js"
10
+ },
11
+ "files": [
12
+ "hood.js",
13
+ "README.md"
14
+ ],
15
+ "sideEffects": false,
16
+ "keywords": [
17
+ "hood",
18
+ "ens",
19
+ "robinhood-chain",
20
+ "name-service",
21
+ "resolver",
22
+ "web3",
23
+ "viem",
24
+ "ethereum"
25
+ ],
26
+ "homepage": "https://hood.ag",
27
+ "license": "MIT",
28
+ "publishConfig": {
29
+ "access": "public"
30
+ }
31
+ }