Hey is a decentralized and permissionless social media app built with Lens Protocol 🌿

refactor: streamline CCIP resolver by removing unused code and enhancing error handling

yoginth.com 2cc90f46 59692a0e

verified
+204 -115
+15 -102
apps/api/src/routes/ens/resolver/ccip.ts
··· 1 1 import type { Context } from "hono"; 2 - import { decodeFunctionData, encodeAbiParameters, type Hex } from "viem"; 3 - import getLensAccount from "@/utils/getLensAccount"; 4 - 5 - const resolverAbi = [ 6 - { 7 - inputs: [ 8 - { name: "name", type: "bytes" }, 9 - { name: "data", type: "bytes" } 10 - ], 11 - name: "resolve", 12 - outputs: [{ type: "bytes" }], 13 - stateMutability: "view", 14 - type: "function" 15 - } 16 - ] as const; 17 - 18 - const hexlify = (data: string) => 19 - data.startsWith("0x") ? (data as Hex) : (`0x${data}` as Hex); 20 - 21 - const decodeDnsName = (nameHex: Hex): string => { 22 - const hex = nameHex.slice(2); 23 - let i = 0; 24 - const labels: string[] = []; 25 - while (i < hex.length) { 26 - const len = Number.parseInt(hex.slice(i, i + 2), 16); 27 - if (len === 0) break; 28 - i += 2; 29 - const labelHex = hex.slice(i, i + len * 2); 30 - const label = Buffer.from(labelHex, "hex").toString("utf8"); 31 - labels.push(label); 32 - i += len * 2; 33 - } 34 - return labels.join("."); 35 - }; 2 + import type { Hex } from "viem"; 3 + import { getRecord } from "./query"; 4 + import { decodeEnsOffchainRequest, encodeEnsOffchainResponse } from "./utils"; 36 5 37 6 const CCIP = async (ctx: Context) => { 38 7 const sender = ctx.req.param("sender"); 39 8 const dataParam = ctx.req.param("data"); 40 9 if (!sender || !dataParam) return ctx.json({ error: "Bad Request" }, 400); 41 10 42 - const callData = hexlify(dataParam); 11 + let result: string; 43 12 44 - let decoded: { functionName: string; args: readonly [Hex, Hex] }; 45 13 try { 46 - decoded = decodeFunctionData({ abi: resolverAbi, data: callData }) as any; 14 + const param = { data: dataParam as Hex, sender: sender as Hex }; 15 + const { name, query } = decodeEnsOffchainRequest(param); 16 + result = await getRecord(name, query); 17 + const data = await encodeEnsOffchainResponse( 18 + param, 19 + result, 20 + process.env.PRIVATE_KEY as Hex 21 + ); 22 + 23 + return ctx.json({ data }, 200); 47 24 } catch { 48 - return ctx.json({ error: "Unsupported calldata" }, 400); 25 + return ctx.json({ error: "Bad Request" }, 400); 49 26 } 50 - 51 - const [dnsName, inner] = decoded.args; 52 - const fqdn = decodeDnsName(dnsName).toLowerCase(); 53 - if (!fqdn.endsWith(".hey.xyz")) 54 - return ctx.json({ error: "Unsupported domain" }, 400); 55 - const label = fqdn.split(".hey.xyz")[0]; 56 - if (!label || label.includes(".")) 57 - return ctx.json({ error: "Invalid label" }, 400); 58 - 59 - const account = await getLensAccount(label); 60 - 61 - const selector = (inner as string).slice(0, 10).toLowerCase(); 62 - let result: Hex | null = null; 63 - 64 - if (selector === "0x3b3b57de") { 65 - const ret = encodeAbiParameters([{ type: "address" }], [account.address]); 66 - result = ret; 67 - } else if (selector === "0xf1cb7e06") { 68 - const addressBytes = account.address.toLowerCase(); 69 - const raw = `0x${addressBytes.slice(2)}` as Hex; 70 - const ret = encodeAbiParameters([{ type: "bytes" }], [raw]); 71 - result = ret; 72 - } else if (selector === "0x59d1d43c") { 73 - const textAbi = [ 74 - { 75 - inputs: [ 76 - { name: "node", type: "bytes32" }, 77 - { name: "key", type: "string" } 78 - ], 79 - name: "text", 80 - outputs: [{ type: "string" }], 81 - stateMutability: "view", 82 - type: "function" 83 - } 84 - ] as const; 85 - 86 - try { 87 - const decodedText = decodeFunctionData({ 88 - abi: textAbi, 89 - data: inner 90 - }) as any; 91 - const key = (decodedText.args?.[1] as string)?.toLowerCase(); 92 - 93 - let value = ""; 94 - if (key === "name") { 95 - value = account.name ?? label; 96 - } else if (key === "avatar") { 97 - value = account.avatar ?? ""; 98 - } else if (key === "bio" || key === "description") { 99 - value = account.bio ?? ""; 100 - } else { 101 - value = ""; 102 - } 103 - 104 - const ret = encodeAbiParameters([{ type: "string" }], [value]); 105 - result = ret; 106 - } catch { 107 - return ctx.json({ error: "Malformed text calldata" }, 400); 108 - } 109 - } else { 110 - return ctx.json({ error: "Unsupported function" }, 400); 111 - } 112 - 113 - return ctx.json({ data: result, signature: "0x" }); 114 27 }; 115 28 116 29 export default CCIP;
+33
apps/api/src/routes/ens/resolver/query.ts
··· 1 + import { zeroAddress } from "viem"; 2 + import getLensAccount from "@/utils/getLensAccount"; 3 + import type { ResolverQuery } from "./utils"; 4 + 5 + export async function getRecord(name: string, query: ResolverQuery) { 6 + const { functionName, args } = query; 7 + 8 + let res: string; 9 + const account = await getLensAccount(name); 10 + 11 + const texts = { 12 + avatar: account.avatar, 13 + bio: account.bio, 14 + name: account.name 15 + }; 16 + 17 + switch (functionName) { 18 + case "addr": { 19 + res = account.address ?? zeroAddress; 20 + break; 21 + } 22 + case "text": { 23 + const key = args[1]; 24 + res = texts[key as keyof typeof texts] ?? ""; 25 + break; 26 + } 27 + default: { 28 + throw new Error(`Unsupported query function ${functionName}`); 29 + } 30 + } 31 + 32 + return res; 33 + }
+153
apps/api/src/routes/ens/resolver/utils.ts
··· 1 + import { 2 + type AbiItem, 3 + type ByteArray, 4 + type Hex, 5 + type Prettify, 6 + serializeSignature 7 + } from "viem"; 8 + import { sign } from "viem/accounts"; 9 + import { 10 + bytesToString, 11 + decodeFunctionData, 12 + encodeAbiParameters, 13 + encodeFunctionResult, 14 + encodePacked, 15 + keccak256, 16 + parseAbi, 17 + toBytes 18 + } from "viem/utils"; 19 + 20 + type ResolverQueryAddr = { 21 + args: 22 + | readonly [nodeHash: `0x${string}`] 23 + | readonly [nodeHash: `0x${string}`, coinType: bigint]; 24 + functionName: "addr"; 25 + }; 26 + 27 + type ResolverQueryText = { 28 + args: readonly [nodeHash: `0x${string}`, key: string]; 29 + functionName: "text"; 30 + }; 31 + 32 + type ResolverQueryContentHash = { 33 + args: readonly [nodeHash: `0x${string}`]; 34 + functionName: "contenthash"; 35 + }; 36 + 37 + export type ResolverQuery = Prettify< 38 + ResolverQueryAddr | ResolverQueryText | ResolverQueryContentHash 39 + >; 40 + 41 + type DecodedRequestFullReturnType = { 42 + name: string; 43 + query: ResolverQuery; 44 + }; 45 + 46 + function 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 + 64 + function dnsDecodeName(encodedName: string): string { 65 + const bytesName = toBytes(encodedName); 66 + return bytesToPacket(bytesName); 67 + } 68 + 69 + const 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 + 73 + const 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 + 80 + export 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 + 104 + export 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 + }
+3 -13
apps/api/src/utils/getLensAccount.ts
··· 2 2 import getAvatar from "@hey/helpers/getAvatar"; 3 3 import { AccountDocument, type AccountFragment } from "@hey/indexer"; 4 4 import apolloClient from "@hey/indexer/apollo/client"; 5 - import type { Hex } from "viem"; 5 + import { type Hex, zeroAddress } from "viem"; 6 6 7 7 const getLensAccount = async ( 8 8 handle: string ··· 23 23 24 24 const address = data.account.owner; 25 25 if (!address) 26 - return { 27 - address: "0x0000000000000000000000000000000000000000", 28 - avatar: "", 29 - bio: "", 30 - name: "" 31 - }; 26 + return { address: zeroAddress, avatar: "", bio: "", name: "" }; 32 27 return { 33 28 address: address.toLowerCase() as Hex, 34 29 avatar: getAvatar(data.account), ··· 36 31 name: getAccount(data.account).name 37 32 }; 38 33 } catch { 39 - return { 40 - address: "0x0000000000000000000000000000000000000000", 41 - avatar: "", 42 - bio: "", 43 - name: "" 44 - }; 34 + return { address: zeroAddress, avatar: "", bio: "", name: "" }; 45 35 } 46 36 }; 47 37