-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,7 @@ import { Deferrable, defineReadOnly, getStatic, resolveProperties } from "@ether | |
import { Transaction } from "@ethersproject/transactions"; | ||
import { sha256 } from "@ethersproject/sha2"; | ||
import { toUtf8Bytes, toUtf8String } from "@ethersproject/strings"; | ||
import { poll } from "@ethersproject/web"; | ||
import { fetchJson, poll } from "@ethersproject/web"; | ||
|
||
import bech32 from "bech32"; | ||
|
||
|
@@ -237,32 +237,59 @@ function base58Encode(data: Uint8Array): string { | |
return Base58.encode(concat([ data, hexDataSlice(sha256(sha256(data)), 0, 4) ])); | ||
} | ||
|
||
export interface Avatar { | ||
url: string; | ||
linkage: Array<{ type: string, content: string }>; | ||
} | ||
|
||
const matchers = [ | ||
new RegExp("^(https):/\/(.*)$", "i"), | ||
new RegExp("^(data):(.*)$", "i"), | ||
new RegExp("^(ipfs):/\/(.*)$", "i"), | ||
new RegExp("^eip155:[0-9]+/(erc[0-9]+):(.*)$", "i"), | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
ricmoo
Author
Member
|
||
]; | ||
|
||
function _parseString(result: string): null | string { | ||
try { | ||
return toUtf8String(_parseBytes(result)); | ||
} catch(error) { } | ||
return null; | ||
} | ||
|
||
function _parseBytes(result: string): null | string { | ||
if (result === "0x") { return null; } | ||
|
||
const offset = BigNumber.from(hexDataSlice(result, 0, 32)).toNumber(); | ||
const length = BigNumber.from(hexDataSlice(result, offset, offset + 32)).toNumber(); | ||
return hexDataSlice(result, offset + 32, offset + 32 + length); | ||
} | ||
|
||
|
||
export class Resolver implements EnsResolver { | ||
readonly provider: BaseProvider; | ||
|
||
readonly name: string; | ||
readonly address: string; | ||
|
||
constructor(provider: BaseProvider, address: string, name: string) { | ||
readonly _resolvedAddress: null | string; | ||
|
||
// The resolvedAddress is only for creating a ReverseLookup resolver | ||
constructor(provider: BaseProvider, address: string, name: string, resolvedAddress?: string) { | ||
defineReadOnly(this, "provider", provider); | ||
defineReadOnly(this, "name", name); | ||
defineReadOnly(this, "address", provider.formatter.address(address)); | ||
defineReadOnly(this, "_resolvedAddress", resolvedAddress); | ||
} | ||
|
||
async _fetchBytes(selector: string, parameters?: string): Promise<string> { | ||
// keccak256("addr(bytes32,uint256)") | ||
const transaction = { | ||
async _fetchBytes(selector: string, parameters?: string): Promise<null | string> { | ||
// e.g. keccak256("addr(bytes32,uint256)") | ||
const tx = { | ||
to: this.address, | ||
data: hexConcat([ selector, namehash(this.name), (parameters || "0x") ]) | ||
}; | ||
|
||
try { | ||
const result = await this.provider.call(transaction); | ||
if (result === "0x") { return null; } | ||
|
||
const offset = BigNumber.from(hexDataSlice(result, 0, 32)).toNumber(); | ||
const length = BigNumber.from(hexDataSlice(result, offset, offset + 32)).toNumber(); | ||
return hexDataSlice(result, offset + 32, offset + 32 + length); | ||
return _parseBytes(await this.provider.call(tx)); | ||
} catch (error) { | ||
if (error.code === Logger.errors.CALL_EXCEPTION) { return null; } | ||
return null; | ||
|
@@ -374,6 +401,95 @@ export class Resolver implements EnsResolver { | |
return address; | ||
} | ||
|
||
async getAvatar(): Promise<null | Avatar> { | ||
const linkage: Array<{ type: string, content: string }> = [ ]; | ||
try { | ||
const avatar = await this.getText("avatar"); | ||
if (avatar == null) { return null; } | ||
|
||
for (let i = 0; i < matchers.length; i++) { | ||
const match = avatar.match(matchers[i]); | ||
|
||
if (match == null) { continue; } | ||
switch (match[1]) { | ||
case "https": | ||
linkage.push({ type: "url", content: avatar }); | ||
return { linkage, url: avatar }; | ||
|
||
case "data": | ||
linkage.push({ type: "data", content: avatar }); | ||
return { linkage, url: avatar }; | ||
|
||
case "ipfs": | ||
linkage.push({ type: "ipfs", content: avatar }); | ||
return { linkage, url: `https:/\/gateway.ipfs.io/ipfs/${ avatar.substring(7) }` } | ||
|
||
case "erc721": | ||
case "erc1155": { | ||
// Depending on the ERC type, use tokenURI(uint256) or url(uint256) | ||
const selector = (match[1] === "erc721") ? "0xc87b56dd": "0x0e89341c"; | ||
linkage.push({ type: match[1], content: avatar }); | ||
|
||
// The owner of this name | ||
const owner = (this._resolvedAddress || await this.getAddress()); | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
ricmoo
Author
Member
|
||
|
||
const comps = (match[2] || "").split("/"); | ||
if (comps.length !== 2) { return null; } | ||
|
||
const addr = await this.provider.formatter.address(comps[0]); | ||
const tokenId = hexZeroPad(BigNumber.from(comps[1]).toHexString(), 32); | ||
|
||
// Check that this account owns the token | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong. |
||
if (match[1] === "erc721") { | ||
// ownerOf(uint256 tokenId) | ||
const tokenOwner = this.provider.formatter.callAddress(await this.provider.call({ | ||
to: addr, data: hexConcat([ "0x6352211e", tokenId ]) | ||
})); | ||
if (owner !== tokenOwner) { return null; } | ||
linkage.push({ type: "owner", content: tokenOwner }); | ||
|
||
} else if (match[1] === "erc1155") { | ||
// balanceOf(address owner, uint256 tokenId) | ||
const balance = BigNumber.from(await this.provider.call({ | ||
to: addr, data: hexConcat([ "0x00fdd58e", hexZeroPad(owner, 32), tokenId ]) | ||
})); | ||
if (balance.isZero()) { return null; } | ||
linkage.push({ type: "balance", content: balance.toString() }); | ||
} | ||
|
||
// Call the token contract for the metadata URL | ||
const tx = { | ||
to: this.provider.formatter.address(comps[0]), | ||
data: hexConcat([ selector, tokenId ]) | ||
}; | ||
let metadataUrl = _parseString(await this.provider.call(tx)) | ||
if (metadataUrl == null) { return null; } | ||
linkage.push({ type: "metadata-url", content: metadataUrl }); | ||
|
||
// ERC-1155 allows a generic {id} in the URL | ||
if (match[1] === "erc1155") { | ||
metadataUrl = metadataUrl.replace("{id}", tokenId.substring(2)); | ||
} | ||
|
||
// Get the token metadata | ||
const metadata = await fetchJson(metadataUrl); | ||
This comment has been minimized.
Sorry, something went wrong.
Arachnid
|
||
|
||
// Pull the image URL out | ||
if (!metadata || typeof(metadata.image) !== "string" || !metadata.image.match(/^https:\/\//i)) { | ||
return null; | ||
} | ||
linkage.push({ type: "metadata", content: JSON.stringify(metadata) }); | ||
linkage.push({ type: "url", content: metadata.image }); | ||
|
||
return { linkage, url: metadata.image }; | ||
} | ||
} | ||
} | ||
} catch (error) { } | ||
|
||
return null; | ||
} | ||
|
||
async getContentHash(): Promise<string> { | ||
|
||
// keccak256("contenthash()") | ||
|
@@ -1615,6 +1731,30 @@ export class BaseProvider extends Provider implements EnsProvider { | |
return name; | ||
} | ||
|
||
async getAvatar(nameOrAddress: string): Promise<null | string> { | ||
let resolver: Resolver = null; | ||
if (isHexString(nameOrAddress)) { | ||
// Address; reverse lookup | ||
const address = this.formatter.address(nameOrAddress); | ||
|
||
const reverseName = address.substring(2).toLowerCase() + ".addr.reverse"; | ||
|
||
const resolverAddress = await this._getResolver(reverseName); | ||
if (!resolverAddress) { return null; } | ||
|
||
resolver = new Resolver(this, resolverAddress, "_", address); | ||
|
||
} else { | ||
// ENS name; forward lookup | ||
resolver = await this.getResolver(nameOrAddress); | ||
} | ||
|
||
const avatar = await resolver.getAvatar(); | ||
if (avatar == null) { return null; } | ||
|
||
return avatar.url; | ||
} | ||
|
||
perform(method: string, params: any): Promise<any> { | ||
return logger.throwError(method + " not implemented", Logger.errors.NOT_IMPLEMENTED, { operation: method }); | ||
} | ||
|
The first parameter here after
eip155:
is chain ID; you should probably check this and return an error if it's not 1.