@@ -14,7 +14,7 @@ import { Deferrable, defineReadOnly, getStatic, resolveProperties } from "@ether
1414import { Transaction } from "@ethersproject/transactions" ;
1515import { sha256 } from "@ethersproject/sha2" ;
1616import { toUtf8Bytes , toUtf8String } from "@ethersproject/strings" ;
17- import { poll } from "@ethersproject/web" ;
17+ import { fetchJson , poll } from "@ethersproject/web" ;
1818
1919import bech32 from "bech32" ;
2020
@@ -237,32 +237,59 @@ function base58Encode(data: Uint8Array): string {
237237 return Base58 . encode ( concat ( [ data , hexDataSlice ( sha256 ( sha256 ( data ) ) , 0 , 4 ) ] ) ) ;
238238}
239239
240+ export interface Avatar {
241+ url : string ;
242+ linkage : Array < { type : string , content : string } > ;
243+ }
244+
245+ const matchers = [
246+ new RegExp ( "^(https):/\/(.*)$" , "i" ) ,
247+ new RegExp ( "^(data):(.*)$" , "i" ) ,
248+ new RegExp ( "^(ipfs):/\/(.*)$" , "i" ) ,
249+ new RegExp ( "^eip155:[0-9]+/(erc[0-9]+):(.*)$" , "i" ) ,
250+ ] ;
251+
252+ function _parseString ( result : string ) : null | string {
253+ try {
254+ return toUtf8String ( _parseBytes ( result ) ) ;
255+ } catch ( error ) { }
256+ return null ;
257+ }
258+
259+ function _parseBytes ( result : string ) : null | string {
260+ if ( result === "0x" ) { return null ; }
261+
262+ const offset = BigNumber . from ( hexDataSlice ( result , 0 , 32 ) ) . toNumber ( ) ;
263+ const length = BigNumber . from ( hexDataSlice ( result , offset , offset + 32 ) ) . toNumber ( ) ;
264+ return hexDataSlice ( result , offset + 32 , offset + 32 + length ) ;
265+ }
266+
267+
240268export class Resolver implements EnsResolver {
241269 readonly provider : BaseProvider ;
242270
243271 readonly name : string ;
244272 readonly address : string ;
245273
246- constructor ( provider : BaseProvider , address : string , name : string ) {
274+ readonly _resolvedAddress : null | string ;
275+
276+ // The resolvedAddress is only for creating a ReverseLookup resolver
277+ constructor ( provider : BaseProvider , address : string , name : string , resolvedAddress ?: string ) {
247278 defineReadOnly ( this , "provider" , provider ) ;
248279 defineReadOnly ( this , "name" , name ) ;
249280 defineReadOnly ( this , "address" , provider . formatter . address ( address ) ) ;
281+ defineReadOnly ( this , "_resolvedAddress" , resolvedAddress ) ;
250282 }
251283
252- async _fetchBytes ( selector : string , parameters ?: string ) : Promise < string > {
253- // keccak256("addr(bytes32,uint256)")
254- const transaction = {
284+ async _fetchBytes ( selector : string , parameters ?: string ) : Promise < null | string > {
285+ // e.g. keccak256("addr(bytes32,uint256)")
286+ const tx = {
255287 to : this . address ,
256288 data : hexConcat ( [ selector , namehash ( this . name ) , ( parameters || "0x" ) ] )
257289 } ;
258290
259291 try {
260- const result = await this . provider . call ( transaction ) ;
261- if ( result === "0x" ) { return null ; }
262-
263- const offset = BigNumber . from ( hexDataSlice ( result , 0 , 32 ) ) . toNumber ( ) ;
264- const length = BigNumber . from ( hexDataSlice ( result , offset , offset + 32 ) ) . toNumber ( ) ;
265- return hexDataSlice ( result , offset + 32 , offset + 32 + length ) ;
292+ return _parseBytes ( await this . provider . call ( tx ) ) ;
266293 } catch ( error ) {
267294 if ( error . code === Logger . errors . CALL_EXCEPTION ) { return null ; }
268295 return null ;
@@ -374,6 +401,95 @@ export class Resolver implements EnsResolver {
374401 return address ;
375402 }
376403
404+ async getAvatar ( ) : Promise < null | Avatar > {
405+ const linkage : Array < { type : string , content : string } > = [ ] ;
406+ try {
407+ const avatar = await this . getText ( "avatar" ) ;
408+ if ( avatar == null ) { return null ; }
409+
410+ for ( let i = 0 ; i < matchers . length ; i ++ ) {
411+ const match = avatar . match ( matchers [ i ] ) ;
412+
413+ if ( match == null ) { continue ; }
414+ switch ( match [ 1 ] ) {
415+ case "https" :
416+ linkage . push ( { type : "url" , content : avatar } ) ;
417+ return { linkage, url : avatar } ;
418+
419+ case "data" :
420+ linkage . push ( { type : "data" , content : avatar } ) ;
421+ return { linkage, url : avatar } ;
422+
423+ case "ipfs" :
424+ linkage . push ( { type : "ipfs" , content : avatar } ) ;
425+ return { linkage, url : `https:/\/gateway.ipfs.io/ipfs/${ avatar . substring ( 7 ) } ` }
426+
427+ case "erc721" :
428+ case "erc1155" : {
429+ // Depending on the ERC type, use tokenURI(uint256) or url(uint256)
430+ const selector = ( match [ 1 ] === "erc721" ) ? "0xc87b56dd" : "0x0e89341c" ;
431+ linkage . push ( { type : match [ 1 ] , content : avatar } ) ;
432+
433+ // The owner of this name
434+ const owner = ( this . _resolvedAddress || await this . getAddress ( ) ) ;
435+
436+ const comps = ( match [ 2 ] || "" ) . split ( "/" ) ;
437+ if ( comps . length !== 2 ) { return null ; }
438+
439+ const addr = await this . provider . formatter . address ( comps [ 0 ] ) ;
440+ const tokenId = hexZeroPad ( BigNumber . from ( comps [ 1 ] ) . toHexString ( ) , 32 ) ;
441+
442+ // Check that this account owns the token
443+ if ( match [ 1 ] === "erc721" ) {
444+ // ownerOf(uint256 tokenId)
445+ const tokenOwner = this . provider . formatter . callAddress ( await this . provider . call ( {
446+ to : addr , data : hexConcat ( [ "0x6352211e" , tokenId ] )
447+ } ) ) ;
448+ if ( owner !== tokenOwner ) { return null ; }
449+ linkage . push ( { type : "owner" , content : tokenOwner } ) ;
450+
451+ } else if ( match [ 1 ] === "erc1155" ) {
452+ // balanceOf(address owner, uint256 tokenId)
453+ const balance = BigNumber . from ( await this . provider . call ( {
454+ to : addr , data : hexConcat ( [ "0x00fdd58e" , hexZeroPad ( owner , 32 ) , tokenId ] )
455+ } ) ) ;
456+ if ( balance . isZero ( ) ) { return null ; }
457+ linkage . push ( { type : "balance" , content : balance . toString ( ) } ) ;
458+ }
459+
460+ // Call the token contract for the metadata URL
461+ const tx = {
462+ to : this . provider . formatter . address ( comps [ 0 ] ) ,
463+ data : hexConcat ( [ selector , tokenId ] )
464+ } ;
465+ let metadataUrl = _parseString ( await this . provider . call ( tx ) )
466+ if ( metadataUrl == null ) { return null ; }
467+ linkage . push ( { type : "metadata-url" , content : metadataUrl } ) ;
468+
469+ // ERC-1155 allows a generic {id} in the URL
470+ if ( match [ 1 ] === "erc1155" ) {
471+ metadataUrl = metadataUrl . replace ( "{id}" , tokenId . substring ( 2 ) ) ;
472+ }
473+
474+ // Get the token metadata
475+ const metadata = await fetchJson ( metadataUrl ) ;
476+
477+ // Pull the image URL out
478+ if ( ! metadata || typeof ( metadata . image ) !== "string" || ! metadata . image . match ( / ^ h t t p s : \/ \/ / i) ) {
479+ return null ;
480+ }
481+ linkage . push ( { type : "metadata" , content : JSON . stringify ( metadata ) } ) ;
482+ linkage . push ( { type : "url" , content : metadata . image } ) ;
483+
484+ return { linkage, url : metadata . image } ;
485+ }
486+ }
487+ }
488+ } catch ( error ) { }
489+
490+ return null ;
491+ }
492+
377493 async getContentHash ( ) : Promise < string > {
378494
379495 // keccak256("contenthash()")
@@ -1615,6 +1731,30 @@ export class BaseProvider extends Provider implements EnsProvider {
16151731 return name ;
16161732 }
16171733
1734+ async getAvatar ( nameOrAddress : string ) : Promise < null | string > {
1735+ let resolver : Resolver = null ;
1736+ if ( isHexString ( nameOrAddress ) ) {
1737+ // Address; reverse lookup
1738+ const address = this . formatter . address ( nameOrAddress ) ;
1739+
1740+ const reverseName = address . substring ( 2 ) . toLowerCase ( ) + ".addr.reverse" ;
1741+
1742+ const resolverAddress = await this . _getResolver ( reverseName ) ;
1743+ if ( ! resolverAddress ) { return null ; }
1744+
1745+ resolver = new Resolver ( this , resolverAddress , "_" , address ) ;
1746+
1747+ } else {
1748+ // ENS name; forward lookup
1749+ resolver = await this . getResolver ( nameOrAddress ) ;
1750+ }
1751+
1752+ const avatar = await resolver . getAvatar ( ) ;
1753+ if ( avatar == null ) { return null ; }
1754+
1755+ return avatar . url ;
1756+ }
1757+
16181758 perform ( method : string , params : any ) : Promise < any > {
16191759 return logger . throwError ( method + " not implemented" , Logger . errors . NOT_IMPLEMENTED , { operation : method } ) ;
16201760 }
0 commit comments