Hey is a decentralized and permissionless social media app built with Lens Protocol 馃尶
1import {
2 type AbiItem,
3 type ByteArray,
4 type Hex,
5 type Prettify,
6 serializeSignature
7} from "viem";
8import { sign } from "viem/accounts";
9import {
10 bytesToString,
11 decodeFunctionData,
12 encodeAbiParameters,
13 encodeFunctionResult,
14 encodePacked,
15 keccak256,
16 parseAbi,
17 toBytes
18} from "viem/utils";
19
20type ResolverQueryAddr = {
21 args:
22 | readonly [nodeHash: `0x${string}`]
23 | readonly [nodeHash: `0x${string}`, coinType: bigint];
24 functionName: "addr";
25};
26
27type ResolverQueryText = {
28 args: readonly [nodeHash: `0x${string}`, key: string];
29 functionName: "text";
30};
31
32type ResolverQueryContentHash = {
33 args: readonly [nodeHash: `0x${string}`];
34 functionName: "contenthash";
35};
36
37export type ResolverQuery = Prettify<
38 ResolverQueryAddr | ResolverQueryText | ResolverQueryContentHash
39>;
40
41type DecodedRequestFullReturnType = {
42 name: string;
43 query: ResolverQuery;
44};
45
46function bytesToPacket(bytes: ByteArray): string {
47 let offset = 0;
48 let result = "";
49
50 while (offset < bytes.length) {
51 const len = bytes[offset];
52 if (len === 0) {
53 offset += 1;
54 break;
55 }
56
57 result += `${bytesToString(bytes.subarray(offset + 1, offset + len + 1))}.`;
58 offset += len + 1;
59 }
60
61 return result.replace(/\.$/, "");
62}
63
64function dnsDecodeName(encodedName: string): string {
65 const bytesName = toBytes(encodedName);
66 return bytesToPacket(bytesName);
67}
68
69const OFFCHAIN_RESOLVER_ABI = parseAbi([
70 "function resolve(bytes calldata name, bytes calldata data) view returns(bytes memory result, uint64 expires, bytes memory sig)"
71]);
72
73const RESOLVER_ABI = parseAbi([
74 "function addr(bytes32 node) view returns (address)",
75 "function addr(bytes32 node, uint256 coinType) view returns (bytes memory)",
76 "function text(bytes32 node, string key) view returns (string memory)",
77 "function contenthash(bytes32 node) view returns (bytes memory)"
78]);
79
80export function decodeEnsOffchainRequest({
81 data
82}: {
83 sender: `0x${string}`;
84 data: `0x${string}`;
85}): DecodedRequestFullReturnType {
86 const decodedResolveCall = decodeFunctionData({
87 abi: OFFCHAIN_RESOLVER_ABI,
88 data
89 });
90
91 const [dnsEncodedName, encodedResolveCall] = decodedResolveCall.args;
92 const name = dnsDecodeName(dnsEncodedName);
93 const query = decodeFunctionData({
94 abi: RESOLVER_ABI,
95 data: encodedResolveCall
96 });
97
98 return {
99 name,
100 query
101 };
102}
103
104export async function encodeEnsOffchainResponse(
105 request: { sender: `0x${string}`; data: `0x${string}` },
106 result: string,
107 signerPrivateKey: Hex
108): Promise<Hex> {
109 const { sender, data } = request;
110 const { query } = decodeEnsOffchainRequest({ data, sender });
111 const ttl = 1000;
112 const validUntil = Math.floor(Date.now() / 1000 + ttl);
113
114 const abiItem: AbiItem | undefined = RESOLVER_ABI.find(
115 (abi) =>
116 abi.name === query.functionName && abi.inputs.length === query.args.length
117 );
118
119 const functionResult = encodeFunctionResult({
120 abi: [abiItem],
121 functionName: query.functionName,
122 result
123 });
124
125 const messageHash = keccak256(
126 encodePacked(
127 ["bytes", "address", "uint64", "bytes32", "bytes32"],
128 [
129 "0x1900",
130 sender,
131 BigInt(validUntil),
132 keccak256(data),
133 keccak256(functionResult)
134 ]
135 )
136 );
137
138 const sig = await sign({
139 hash: messageHash,
140 privateKey: signerPrivateKey
141 });
142
143 const encodedResponse = encodeAbiParameters(
144 [
145 { name: "result", type: "bytes" },
146 { name: "expires", type: "uint64" },
147 { name: "sig", type: "bytes" }
148 ],
149 [functionResult, BigInt(validUntil), serializeSignature(sig)]
150 );
151
152 return encodedResponse;
153}