Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: migrate basename action #176

Merged
merged 1 commit into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { encodeFunctionData, namehash, parseEther } from "viem";

import { basenameActionProvider } from "./basenameActionProvider";
import {
BASENAMES_REGISTRAR_CONTROLLER_ADDRESS_MAINNET,
REGISTRATION_DURATION,
L2_RESOLVER_ADDRESS_MAINNET,
L2_RESOLVER_ABI,
REGISTRAR_ABI,
BASENAMES_REGISTRAR_CONTROLLER_ADDRESS_TESTNET,
L2_RESOLVER_ADDRESS_TESTNET,
} from "./constants";
import { RegisterBasenameSchema } from "./schemas";
import { EvmWalletProvider } from "../../wallet_providers";
import { Coinbase } from "@coinbase/coinbase-sdk";

const MOCK_AMOUNT = "0.123";
const MOCK_BASENAME = "test-basename";

describe("Register Basename Input", () => {
it("should successfully parse valid input", () => {
const validInput = {
amount: MOCK_AMOUNT,
basename: MOCK_BASENAME,
};

const result = RegisterBasenameSchema.safeParse(validInput);

expect(result.success).toBe(true);
expect(result.data).toEqual(validInput);
});

it("should fail parsing empty input", () => {
const emptyInput = {};
const result = RegisterBasenameSchema.safeParse(emptyInput);

expect(result.success).toBe(false);
});
});

describe("Register Basename Action", () => {
/**
* This is the default network.
*/
const NETWORK_ID = Coinbase.networks.BaseMainnet;

/**
* This is a 40 character hexadecimal string that requires lowercase alpha characters.
*/
const ADDRESS_ID = "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83";

let mockWallet: jest.Mocked<EvmWalletProvider>;

const actionProvider = basenameActionProvider();

beforeEach(() => {
mockWallet = {
getAddress: jest.fn().mockReturnValue(ADDRESS_ID),
getNetwork: jest.fn().mockReturnValue({ networkId: NETWORK_ID }),
sendTransaction: jest.fn(),
waitForTransactionReceipt: jest.fn(),
} as unknown as jest.Mocked<EvmWalletProvider>;

mockWallet.sendTransaction.mockResolvedValue("some-hash" as `0x${string}`);
mockWallet.waitForTransactionReceipt.mockResolvedValue({});
});

it(`should Successfully respond with ${MOCK_BASENAME}.base.eth for network: ${Coinbase.networks.BaseMainnet}`, async () => {
const args = {
amount: MOCK_AMOUNT,
basename: MOCK_BASENAME,
};

const name = `${MOCK_BASENAME}.base.eth`;

mockWallet.getNetwork.mockReturnValue({
protocolFamily: "evm",
networkId: Coinbase.networks.BaseMainnet,
});

const response = await actionProvider.register(mockWallet, args);

expect(mockWallet.sendTransaction).toHaveBeenCalledWith({
to: BASENAMES_REGISTRAR_CONTROLLER_ADDRESS_MAINNET,
data: encodeFunctionData({
abi: REGISTRAR_ABI,
functionName: "register",
args: [
{
name: MOCK_BASENAME,
owner: ADDRESS_ID,
duration: REGISTRATION_DURATION,
resolver: L2_RESOLVER_ADDRESS_MAINNET,
data: [
encodeFunctionData({
abi: L2_RESOLVER_ABI,
functionName: "setAddr",
args: [namehash(name), ADDRESS_ID],
}),
encodeFunctionData({
abi: L2_RESOLVER_ABI,
functionName: "setName",
args: [namehash(name), name],
}),
],
reverseRecord: true,
},
],
}),
value: parseEther(MOCK_AMOUNT),
});
expect(mockWallet.waitForTransactionReceipt).toHaveBeenCalledWith("some-hash");
expect(response).toContain(`Successfully registered basename ${MOCK_BASENAME}.base.eth`);
expect(response).toContain(`for address ${ADDRESS_ID}`);
});

it(`should Successfully respond with ${MOCK_BASENAME}.basetest.eth for any other network`, async () => {
const args = {
amount: MOCK_AMOUNT,
basename: MOCK_BASENAME,
};

const name = `${MOCK_BASENAME}.basetest.eth`;

mockWallet.getNetwork.mockReturnValue({
protocolFamily: "evm",
networkId: "anything-else",
});

const response = await actionProvider.register(mockWallet, args);

expect(mockWallet.sendTransaction).toHaveBeenCalledWith({
to: BASENAMES_REGISTRAR_CONTROLLER_ADDRESS_TESTNET,
data: encodeFunctionData({
abi: REGISTRAR_ABI,
functionName: "register",
args: [
{
name: MOCK_BASENAME,
owner: ADDRESS_ID,
duration: REGISTRATION_DURATION,
resolver: L2_RESOLVER_ADDRESS_TESTNET,
data: [
encodeFunctionData({
abi: L2_RESOLVER_ABI,
functionName: "setAddr",
args: [namehash(name), ADDRESS_ID],
}),
encodeFunctionData({
abi: L2_RESOLVER_ABI,
functionName: "setName",
args: [namehash(name), name],
}),
],
reverseRecord: true,
},
],
}),
value: parseEther(MOCK_AMOUNT),
});
expect(mockWallet.waitForTransactionReceipt).toHaveBeenCalledWith("some-hash");
expect(response).toContain(`Successfully registered basename ${MOCK_BASENAME}.basetest.eth`);
expect(response).toContain(`for address ${ADDRESS_ID}`);
});

it("should fail with an error", async () => {
const args = {
amount: MOCK_AMOUNT,
basename: MOCK_BASENAME,
};

const error = new Error("Failed to register basename");
mockWallet.sendTransaction.mockRejectedValue(error);

await actionProvider.register(mockWallet, args);

expect(mockWallet.sendTransaction).toHaveBeenCalled();
expect(`Error registering basename: ${error}`);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { encodeFunctionData, Hex, namehash, parseEther } from "viem";
import { z } from "zod";
import { ActionProvider } from "../action_provider";
import { Network } from "../../wallet_providers/wallet_provider";
import { CreateAction } from "../action_decorator";
import {
L2_RESOLVER_ADDRESS_MAINNET,
L2_RESOLVER_ADDRESS_TESTNET,
L2_RESOLVER_ABI,
REGISTRATION_DURATION,
BASENAMES_REGISTRAR_CONTROLLER_ADDRESS_MAINNET,
BASENAMES_REGISTRAR_CONTROLLER_ADDRESS_TESTNET,
REGISTRAR_ABI,
} from "./constants";
import { RegisterBasenameSchema } from "./schemas";
import { EvmWalletProvider } from "../../wallet_providers";

/**
* Action provider for registering Basenames.
*/
export class BasenameActionProvider extends ActionProvider {
/**
* Constructs a new BasenameActionProvider.
*/
constructor() {
super("basename", []);
}

/**
* Registers a Basename.
*
* @param wallet - The wallet to use for the registration.
* @param args - The arguments for the registration.
* @returns A string indicating the success or failure of the registration.
*/
@CreateAction({
name: "registerBasename",
description: `
This tool will register a Basename for the agent. The agent should have a wallet associated to register a Basename.
When your network ID is 'base-mainnet' (also sometimes known simply as 'base'), the name must end with .base.eth, and when your network ID is 'base-sepolia', it must ends with .basetest.eth.
Do not suggest any alternatives and never try to register a Basename with another postfix. The prefix of the name must be unique so if the registration of the
Basename fails, you should prompt to try again with a more unique name.
`,
schema: RegisterBasenameSchema,
})
async register(
wallet: EvmWalletProvider,
args: z.infer<typeof RegisterBasenameSchema>,
): Promise<string> {
const address = wallet.getAddress();
const isMainnet = wallet.getNetwork().networkId === "base-mainnet";

const suffix = isMainnet ? ".base.eth" : ".basetest.eth";
if (!args.basename.endsWith(suffix)) {
args.basename += suffix;
}

const l2ResolverAddress = isMainnet ? L2_RESOLVER_ADDRESS_MAINNET : L2_RESOLVER_ADDRESS_TESTNET;

const addressData = encodeFunctionData({
abi: L2_RESOLVER_ABI,
functionName: "setAddr",
args: [namehash(args.basename), address],
});
const nameData = encodeFunctionData({
abi: L2_RESOLVER_ABI,
functionName: "setName",
args: [namehash(args.basename), args.basename],
});

try {
const contractAddress = isMainnet
? BASENAMES_REGISTRAR_CONTROLLER_ADDRESS_MAINNET
: BASENAMES_REGISTRAR_CONTROLLER_ADDRESS_TESTNET;

const hash = await wallet.sendTransaction({
to: contractAddress,
data: encodeFunctionData({
abi: REGISTRAR_ABI,
functionName: "register",
args: [
{
name: args.basename.replace(suffix, ""),
owner: address as Hex,
duration: REGISTRATION_DURATION,
resolver: l2ResolverAddress,
data: [addressData, nameData],
reverseRecord: true,
},
],
}),
value: parseEther(args.amount),
});

await wallet.waitForTransactionReceipt(hash);

return `Successfully registered basename ${args.basename} for address ${address}`;
} catch (error) {
return `Error registering basename: Error: ${error}`;
}
}

/**
* Checks if the Basename action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the Basename action provider supports the network, false otherwise.
*/
supportsNetwork = (network: Network) =>
network.networkId === "base-mainnet" || network.networkId === "base-sepolia";
}

export const basenameActionProvider = () => new BasenameActionProvider();
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Contract addresses
export const BASENAMES_REGISTRAR_CONTROLLER_ADDRESS_MAINNET =
"0x4cCb0BB02FCABA27e82a56646E81d8c5bC4119a5";
export const BASENAMES_REGISTRAR_CONTROLLER_ADDRESS_TESTNET =
"0x49aE3cC2e3AA768B1e5654f5D3C6002144A59581";

export const L2_RESOLVER_ADDRESS_MAINNET = "0xC6d566A56A1aFf6508b41f6c90ff131615583BCD";
export const L2_RESOLVER_ADDRESS_TESTNET = "0x6533C94869D28fAA8dF77cc63f9e2b2D6Cf77eBA";

// Default registration duration (1 year in seconds)
export const REGISTRATION_DURATION = 31557600n;

// Relevant ABI for L2 Resolver Contract.
export const L2_RESOLVER_ABI = [
{
inputs: [
{ internalType: "bytes32", name: "node", type: "bytes32" },
{ internalType: "address", name: "a", type: "address" },
],
name: "setAddr",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{ internalType: "bytes32", name: "node", type: "bytes32" },
{ internalType: "string", name: "newName", type: "string" },
],
name: "setName",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
];

// Relevant ABI for Basenames Registrar Controller Contract.
export const REGISTRAR_ABI = [
{
inputs: [
{
components: [
{
internalType: "string",
name: "name",
type: "string",
},
{
internalType: "address",
name: "owner",
type: "address",
},
{
internalType: "uint256",
name: "duration",
type: "uint256",
},
{
internalType: "address",
name: "resolver",
type: "address",
},
{
internalType: "bytes[]",
name: "data",
type: "bytes[]",
},
{
internalType: "bool",
name: "reverseRecord",
type: "bool",
},
],
internalType: "struct RegistrarController.RegisterRequest",
name: "request",
type: "tuple",
},
],
name: "register",
outputs: [],
stateMutability: "payable",
type: "function",
},
] as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./basenameActionProvider";
export * from "./schemas";
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { z } from "zod";

/**
* Input schema for registering a Basename.
*/
export const RegisterBasenameSchema = z
.object({
basename: z.string().describe("The Basename to assign to the agent"),
amount: z.string().default("0.002").describe("The amount of ETH to pay for registration"),
})
.strip()
.describe("Instructions for registering a Basename");