Skip to content
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@snapshot-labs/snapshot.js",
"version": "0.12.55",
"version": "0.12.56",
"repository": "snapshot-labs/snapshot.js",
"license": "MIT",
"main": "dist/snapshot.cjs.js",
Expand Down
10 changes: 9 additions & 1 deletion src/utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
getEnsTextRecord
} from './utils';

const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000';

vi.mock('cross-fetch', async () => {
const actual = await vi.importActual('cross-fetch');

Expand Down Expand Up @@ -617,7 +619,13 @@ describe('utils', () => {
describe('getEnsOwner', () => {
test('should return null when the ENS is not valid', () => {
// special hidden characters after the k
expect(getEnsOwner('elonmusk‍‍.eth')).resolves.toBe(null);
expect(getEnsOwner('elonmusk‍‍.eth')).resolves.toBe(EMPTY_ADDRESS);
});

test('throw an error when the network is not supported', () => {
expect(getEnsOwner('shot.eth', '100')).rejects.toThrow(
'Network not supported'
);
});
});

Expand Down
82 changes: 78 additions & 4 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ interface Strategy {
params: any;
}

type DomainType = 'ens' | 'tld' | 'other-tld' | 'subdomain';

const MUTED_ERRORS = [
// mute error from coinbase, when the subdomain is not found
// most other resolvers just return an empty address
'response not found during CCIP fetch',
// mute error from missing offchain resolver (mostly for sepolia)
'UNSUPPORTED_OPERATION'
];
const ENS_REGISTRY = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e';
const ENS_ABI = [
'function text(bytes32 node, string calldata key) external view returns (string memory)',
Expand Down Expand Up @@ -219,6 +228,45 @@ ajv.addFormat('domain', {
}
});

function getDomainType(domain: string): DomainType {
const isEns = domain.endsWith('.eth');

const tokens = domain.split('.');

if (tokens.length === 1) return 'tld';
else if (tokens.length === 2 && !isEns) return 'other-tld';
else if (tokens.length > 2) return 'subdomain';
else if (isEns) return 'ens';
else throw new Error('Invalid domain');
}

// see https://docs.ens.domains/registry/dns#gasless-import
async function getDNSOwner(domain: string): Promise<string> {
const response = await fetch(
`https://cloudflare-dns.com/dns-query?name=${domain}&type=TXT`,
{
headers: {
accept: 'application/dns-json'
}
}
);

const data = await response.json();
// Error list: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6
if (data.Status === 3) return EMPTY_ADDRESS;
if (data.Status !== 0) throw new Error('Failed to fetch DNS Owner');

const ownerRecord = data.Answer?.find((record: any) =>
record.data.includes('ENS1')
);

if (!ownerRecord) return EMPTY_ADDRESS;

return getAddress(
ownerRecord.data.replace(new RegExp('"', 'g'), '').split(' ').pop()
);
}

export async function call(provider, abi: any[], call: any[], options?) {
const contract = new Contract(call[0], abi, provider);
try {
Expand Down Expand Up @@ -611,7 +659,12 @@ export async function getEnsOwner(
ens: string,
network = '1',
options: any = {}
): Promise<string | null> {
): Promise<string> {
if (!networks[network]?.ensResolvers?.length) {
throw new Error('Network not supported');
}

const domainType = getDomainType(ens);
const provider = getProvider(network, options);
const ensRegistry = new Contract(
ENS_REGISTRY,
Expand All @@ -624,7 +677,7 @@ export async function getEnsOwner(
try {
ensHash = namehash(ensNormalize(ens));
} catch (e: any) {
return null;
return EMPTY_ADDRESS;
}

const ensNameWrapper =
Expand All @@ -639,14 +692,35 @@ export async function getEnsOwner(
);
owner = await ensNameWrapperContract.ownerOf(ensHash);
}
return owner;

if (owner === EMPTY_ADDRESS && domainType === 'other-tld') {
const resolvedAddress = await provider.resolveName(ens);

// Filter out domains with valid TXT records, but not imported
if (resolvedAddress) {
owner = await getDNSOwner(ens);
}
}

if (owner === EMPTY_ADDRESS && domainType === 'subdomain') {
try {
owner = await provider.resolveName(ens);
} catch (e: any) {
if (MUTED_ERRORS.every((error) => !e.message.includes(error))) {
throw e;
}
owner = EMPTY_ADDRESS;
}
}

return owner || EMPTY_ADDRESS;
}

export async function getSpaceController(
id: string,
network = '1',
options: any = {}
): Promise<string | null> {
): Promise<string> {
const spaceUri = await getSpaceUri(id, network, options);
if (spaceUri) {
let isUriAddress = isAddress(spaceUri);
Expand Down
76 changes: 76 additions & 0 deletions test/e2e/utils.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, test, expect } from 'vitest';
import { getEnsOwner } from '../../src/utils';

const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000';

describe('utils', () => {
describe('getEnsOwner', () => {
describe('onchain resolver', () => {
test('return an address for mainnet', () => {
expect(getEnsOwner('shot.eth', '1')).resolves.toBe(
'0x8C28Cf33d9Fd3D0293f963b1cd27e3FF422B425c'
);
});

test('return an address for sepolia', () => {
expect(getEnsOwner('snapshot.eth', '11155111')).resolves.toBe(
'0x8C28Cf33d9Fd3D0293f963b1cd27e3FF422B425c'
);
});

test('return an address for subdomain', () => {
expect(getEnsOwner('2.snapspace.eth')).resolves.toBe(
'0x24F15402C6Bb870554489b2fd2049A85d75B982f'
);
});

test('return an address for other TLD', () => {
expect(getEnsOwner('worldlibertyfinancial.com')).resolves.toBe(
'0x407F66Afb4f9876637AcCC3246099a2f9705c178'
);
});

test('return an empty address for non-existent subdomain', () => {
expect(getEnsOwner('2arst.snapspace.eth')).resolves.toBe(EMPTY_ADDRESS);
});
});

describe('offchain resolver', () => {
test('return an address for coinbase resolver', () => {
expect(getEnsOwner('lucemans.cb.id')).resolves.toBe(
'0x4e7abb71BEe38011c54c30D0130c0c71Da09222b'
);
});

test('return an address for uniswap resolver', () => {
expect(getEnsOwner('lucemans.uni.eth')).resolves.toBe(
'0x225f137127d9067788314bc7fcc1f36746a3c3B5'
);
});

test('return an empty address when no result from resolver on mainnet', () => {
expect(getEnsOwner('notfounddomain.uni.eth')).resolves.toBe(
EMPTY_ADDRESS
);
});

test('return an empty address when no result from resolver on testnet', () => {
expect(getEnsOwner('notfounddomain.uni.eth', '11155111')).resolves.toBe(
EMPTY_ADDRESS
);
});
});

describe('offchain DNS resolver', () => {
test('return an address for claimed domain', () => {
expect(getEnsOwner('defi.app')).resolves.toBe(
'0x7aeB96261e9dC2C9f01BaE6A516Df80a5a98c7eB'
);
});

test('return an empty address for unclaimed domain', () => {
expect(getEnsOwner('google.com')).resolves.toBe(EMPTY_ADDRESS);
});
});
});
});