Skip to content

Commit

Permalink
[TS SDK] Feature: L2 ENS name resolution (#4334)
Browse files Browse the repository at this point in the history
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds L2 ENS name resolution functionality to the Thirdweb package.

### Detailed summary
- Added L2 ENS name resolution
- Updated cache key in resolve-name.ts
- Added new constants in constants.ts
- Exported new functions and constants in ens.ts
- Updated resolve-address.ts and resolve-l2-name.ts for Basename resolution
- Added resolve-l2-name test cases
- Added resolve-l2-name functionality and related files

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
  • Loading branch information
gregfromstl committed Aug 30, 2024
1 parent c52e1be commit 6432e8d
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/wise-hornets-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": minor
---

Adds L2 ENS name resolution
3 changes: 3 additions & 0 deletions packages/thirdweb/scripts/generate/abis/ens/L2Resolver.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
"function name(bytes32 node) view returns (string memory)"
]
10 changes: 10 additions & 0 deletions packages/thirdweb/src/exports/extensions/ens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,13 @@ export {
type ResolveNameOptions,
resolveName,
} from "../../extensions/ens/resolve-name.js";

export {
type ResolveL2NameOptions,
resolveL2Name,
} from "../../extensions/ens/resolve-l2-name.js";

export {
BASENAME_RESOLVER_ADDRESS,
BASE_SEPOLIA_BASENAME_RESOLVER_ADDRESS,
} from "../../extensions/ens/constants.js";

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/thirdweb/src/extensions/ens/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export const UNIVERSAL_RESOLVER_ADDRESS =
"0xce01f8eee7E479C928F8919abD53E553a36CeF67";
export const BASENAME_RESOLVER_ADDRESS =
"0xC6d566A56A1aFf6508b41f6c90ff131615583BCD";
export const BASE_SEPOLIA_BASENAME_RESOLVER_ADDRESS =
"0x6533C94869D28fAA8dF77cc63f9e2b2D6Cf77eBA";
13 changes: 13 additions & 0 deletions packages/thirdweb/src/extensions/ens/resolve-address.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { describe, expect, it } from "vitest";
import { TEST_CLIENT } from "../../../test/src/test-clients.js";
import { base } from "../../chains/chain-definitions/base.js";
import { BASENAME_RESOLVER_ADDRESS } from "./constants.js";
import { resolveAddress } from "./resolve-address.js";

// skip this test suite if there is no secret key available to test with
Expand All @@ -22,4 +24,15 @@ describe.runIf(process.env.TW_SECRET_KEY)("ENS:resolve-address", () => {
});
expect(address).toBe("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
});

it("should resolve Basename", async () => {
const name = "myk.base.eth";
const address = await resolveAddress({
client: TEST_CLIENT,
name,
resolverChain: base,
resolverAddress: BASENAME_RESOLVER_ADDRESS,
});
expect(address).toBe("0x653Ff253b0c7C1cc52f484e891b71f9f1F010Bfb");
});
});
14 changes: 13 additions & 1 deletion packages/thirdweb/src/extensions/ens/resolve-address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ export type ResolveAddressOptions = {
* name: "vitalik.eth",
* });
* ```
*
* Resolve an address to a Basename.
* ```ts
* import { resolveAddress, BASENAME_RESOLVER_ADDRESS } from "thirdweb/extensions/ens";
* import { base } from "thirdweb/chains";
* const address = await resolveAddress({
* client,
* name: "myk.base.eth",
* resolverAddress: BASENAME_RESOLVER_ADDRESS,
* resolverChain: base,
* });
* ```
* @extension ENS
* @returns A promise that resolves to the Ethereum address.
*/
Expand All @@ -58,7 +70,7 @@ export async function resolveAddress(options: ResolveAddressOptions) {
return resolvedAddress;
},
{
cacheKey: `ens:addr:${name}`,
cacheKey: `ens:addr:${resolverChain?.id || 1}:${name}`,
// 1min cache
cacheTime: 60 * 1000,
},
Expand Down
30 changes: 30 additions & 0 deletions packages/thirdweb/src/extensions/ens/resolve-l2-name.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { TEST_CLIENT } from "../../../test/src/test-clients.js";
import { base } from "../../chains/chain-definitions/base.js";
import { BASENAME_RESOLVER_ADDRESS } from "./constants.js";
import { resolveL2Name } from "./resolve-l2-name.js";

// skip this test suite if there is no secret key available to test with
// TODO: remove reliance on secret key during unit tests entirely
describe.runIf(process.env.TW_SECRET_KEY)("ENS:resolve-l2-name", () => {
it("should resolve Basename", async () => {
const ens = await resolveL2Name({
client: TEST_CLIENT,
// myk.base.eth
address: "0x653Ff253b0c7C1cc52f484e891b71f9f1F010Bfb",
resolverChain: base,
resolverAddress: BASENAME_RESOLVER_ADDRESS,
});
expect(ens).toBe("myk.base.eth");
});

it("should return null if no Basename exists for the address", async () => {
const ens = await resolveL2Name({
client: TEST_CLIENT,
address: "0xc6248746A9CA5935ae722E2061347A5897548c03",
resolverChain: base,
resolverAddress: BASENAME_RESOLVER_ADDRESS,
});
expect(ens).toBeNull();
});
});
107 changes: 107 additions & 0 deletions packages/thirdweb/src/extensions/ens/resolve-l2-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { Address } from "abitype";
import { type Hex, encodePacked, keccak256, namehash } from "viem";
import type { Chain } from "../../chains/types.js";
import type { ThirdwebClient } from "../../client/client.js";
import { getContract } from "../../contract/contract.js";
import { withCache } from "../../utils/promise/withCache.js";
import { name } from "./__generated__/L2Resolver/read/name.js";

/**
* @extension ENS
*/
export type ResolveL2NameOptions = {
client: ThirdwebClient;
address: Address;
resolverAddress: string;
resolverChain: Chain;
};

/**
* Convert an address to a reverse node for ENS resolution
*
* @internal
*/
export const convertReverseNodeToBytes = (
address: Address,
chainId: number,
) => {
const addressFormatted = address.toLocaleLowerCase() as Address;
const addressNode = keccak256(addressFormatted.substring(2) as Hex);
const cointype = (0x80000000 | chainId) >>> 0;

const chainCoinType = cointype.toString(16).toLocaleUpperCase();
const reverseNode = namehash(`${chainCoinType.toLocaleUpperCase()}.reverse`);

const addressReverseNode = keccak256(
encodePacked(["bytes32", "bytes32"], [reverseNode, addressNode]),
);
return addressReverseNode;
};

/**
* Resolves the L2 name for a specified address.
* @param options - The options for resolving an L2 ENS address.
* @example
* ```ts
* import { resolveL2Name } from "thirdweb/extensions/ens";
* const name = await resolveL2Name({
* client,
* address: "0x1234...",
* resolverAddress: "0x...",
* resolverChain: base,
* });
* ```
*
* Resolve a Basename.
* ```ts
* import { resolveL2Name, BASENAME_RESOLVER_ADDRESS } from "thirdweb/extensions/ens";
* import { base } from "thirdweb/chains";
* const name = await resolveL2Name({
* client,
* address: "0x1234...",
* resolverAddress: BASENAME_RESOLVER_ADDRESS,
* resolverChain: base,
* });
* ```
* @extension ENS
* @returns A promise that resolves to the Ethereum address.
*/
export async function resolveL2Name(options: ResolveL2NameOptions) {
const { client, address, resolverAddress, resolverChain } = options;

return withCache(
async () => {
const contract = getContract({
client,
chain: resolverChain,
address: resolverAddress,
});

const reverseName = convertReverseNodeToBytes(
address,
resolverChain.id || 1,
);

const resolvedName = await name({
contract,
node: reverseName,
}).catch((e) => {
if ("data" in e && e.data === "0x7199966d") {
return null;
}
throw e;

Check warning on line 92 in packages/thirdweb/src/extensions/ens/resolve-l2-name.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/extensions/ens/resolve-l2-name.ts#L89-L92

Added lines #L89 - L92 were not covered by tests
});

if (resolvedName === "") {
return null;
}

return resolvedName;
},
{
cacheKey: `ens:name:${resolverChain}:${address}`,
// 1min cache
cacheTime: 60 * 1000,
},
);
}
2 changes: 1 addition & 1 deletion packages/thirdweb/src/extensions/ens/resolve-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export async function resolveName(options: ResolveNameOptions) {
return name;
},
{
cacheKey: `ens:name:${address}`,
cacheKey: `ens:name:${resolverChain?.id || 1}:${address}`,
// 1min cache
cacheTime: 60 * 1000,
},
Expand Down

0 comments on commit 6432e8d

Please sign in to comment.