Skip to content

Commit

Permalink
Add new field: nft.owner (#78)
Browse files Browse the repository at this point in the history
It represents the current owner of the NFT.

Note that some NFT implementations don’t provide a .ownerOf() method,
in which case .owner will return an empty string.
  • Loading branch information
bpierre authored May 19, 2021
1 parent 35fc4bd commit a3ee65f
Show file tree
Hide file tree
Showing 19 changed files with 123 additions and 74 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ function Nft() {
<h1>{nft.name}</h1>
<img src={nft.image} alt="" />
<p>{nft.description}</p>
<p>Owner: {nft.owner}</p>
</section>
)
}
Expand Down Expand Up @@ -121,6 +122,9 @@ result.nft.description

// image / media URL of the NFT (or empty string)
result.nft.image

// current owner of the NFT (or empty string)
result.nft.owner
```

As TypeScript type:
Expand All @@ -132,9 +136,10 @@ type NftResult = {
reload: () => void
error?: Error
nft?: {
name: string
description: string
image: string
name: string
owner: string
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion examples/ethereum/src/Nft.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ function NftDetails({ nft }: { nft?: NftMetadata }) {
return null
}

const { image } = nft
const name = nft.name || "Untitled"
const description = nft.description || "−"
const image = nft.image || ""
return (
<>
<div
Expand Down
2 changes: 1 addition & 1 deletion examples/ethers/src/Nft.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ function NftDetails({ nft }: { nft?: NftMetadata }) {
return null
}

const { image, } = nft
const name = nft.name || "Untitled"
const description = nft.description || "−"
const image = nft.image || ""
return (
<>
<div
Expand Down
2 changes: 2 additions & 0 deletions examples/nfts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default shuffle<[string, string, string, string]>([
"https://market.decentraland.org/contracts/0x32b7495895264ac9d0b12d32afd435453458b1c6/tokens/4087",
],

// ERC-1155
[
"0xd07dc4262bcdbf85190c01c996b4c06a461d2430",
"90473",
Expand Down Expand Up @@ -76,6 +77,7 @@ export default shuffle<[string, string, string, string]>([
"https://niftygateway.com/itemdetail/secondary/0x1a9efe6d9a7a977a938f03b1a549bdd9cd316432/11000010006",
],

// ERC-1155
[
"0x495f947276749ce646f68ac8c248420045cb7b5e",
"63990428236934811571513178702512145453357596655980286527887248477662016962561",
Expand Down
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,15 @@
"*.tsx"
],
"rules": {
"import/no-default-export": "off"
"import/no-default-export": "off",
"import/no-unresolved": [
"error",
{
"ignore": [
"^react$"
]
}
]
}
}
]
Expand Down
12 changes: 9 additions & 3 deletions src/fetchers/ethereum/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from "../shared/decentraland-parcel"
import { moonCatsMetadata, isMoonCats } from "../shared/mooncats"
import { moonCatsCatId } from "./mooncats"
import { fetchStandardNftUrl } from "./standard-nft"
import { fetchStandardNftContractData } from "./standard-nft"

export default function ethereumFetcher(
config: EthereumFetcherConfig
Expand Down Expand Up @@ -50,9 +50,15 @@ export default function ethereumFetcher(
return moonCatsMetadata(tokenId, moonCatsCatId(config))
}

return fetchMetadata(
await fetchStandardNftUrl(contractAddress, tokenId, config)
const [metadataUrl, owner] = await fetchStandardNftContractData(
contractAddress,
tokenId,
config
)

const metadata = await fetchMetadata(metadataUrl)

return { ...metadata, owner }
},
}
}
34 changes: 24 additions & 10 deletions src/fetchers/ethereum/standard-nft.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import { CRYPTOVOXELS } from "../../known-contracts"
import {
ERC1155_ID,
ERC721_ID,
decodeString,
decodeAddress,
decodeBoolean,
decodeString,
ethCall,
methodOwnerOfErc721,
methodUriErc1155,
methodUriErc721,
supportsInterfaceMethodErc165,
Expand All @@ -18,25 +20,37 @@ import {
// but are missing ERC165’s supportsInterface()
const KNOWN_ERC721_LIKE = [CRYPTOVOXELS]

export async function fetchStandardNftUrl(
export async function fetchStandardNftContractData(
contractAddress: Address,
tokenId: string,
{ ethereum }: EthereumFetcherConfig
): Promise<string> {
): Promise<[string, Address]> {
if (!ethereum) {
return ""
return ["", ""]
}

const urlCall = async (method: string): Promise<string> => {
const result = await ethCall(ethereum, contractAddress, method)
return normalizeTokenUrl(decodeString(result), tokenId)
}

const isKnown721 = KNOWN_ERC721_LIKE.some((address) =>
// call ownerOf() even on 1155 contracts, just in case it exists
const ownerPromise = ethCall(
ethereum,
contractAddress,
methodOwnerOfErc721(BigInt(tokenId))
)
.then(decodeAddress)
.catch(() => "")

const isKnown721Like = KNOWN_ERC721_LIKE.some((address) =>
addressesEqual(address, contractAddress)
)
if (isKnown721) {
return urlCall(methodUriErc721(BigInt(tokenId)))
if (isKnown721Like) {
return Promise.all([
urlCall(methodUriErc721(BigInt(tokenId))),
ownerPromise,
])
}

const calls = [
Expand All @@ -60,15 +74,15 @@ export async function fetchStandardNftUrl(
supportsMethod
).then(decodeBoolean)

// throw for the Promise.any()
// throw for the Promise.any() to skip this branch
if (!supported) {
throw new Error("Unsupported method")
}

return urlCall(uriMethod)
return Promise.all([urlCall(uriMethod), ownerPromise])
})
)
} catch (err) {
return ""
return ["", ""]
}
}
18 changes: 18 additions & 0 deletions src/fetchers/ethereum/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { EthereumProviderEip1193 } from "./types"
// See https://docs.soliditylang.org/en/v0.5.3/abi-spec.html#function-selector-and-argument-encoding
const URI_METHOD_ERC721 = "0xc87b56dd" // tokenURI(uint256)
const URI_METHOD_ERC1155 = "0x0e89341c" // uri(uint256)
const OWNER_OF_METHOD_ERC721 = "0x6352211e" // ownerOf(uint256)
const SUPPORTS_INTERFACE_METHOD_ERC165 = "0x01ffc9a7" // supportsInterface(bytes4)

// ERC165 identifiers
Expand Down Expand Up @@ -49,6 +50,19 @@ export function decodeString(hex: string): string {
return new TextDecoder().decode(bytes)
}

export function decodeAddress(hex: string): string {
const data = hexToUint8Array(hex)
const bytes = data.subarray(0, 32)
const decoded = bytesToBigInt(bytes)
if (decoded >= BigInt(2) ** BigInt(160))
throw new Error(
`Encoded value is bigger than the largest possible address. Decoded value: 0x${decoded.toString(
16
)}.`
)
return `0x${decoded.toString(16)}`
}

export function methodUriErc721(tokenId: bigint): string {
return URI_METHOD_ERC721 + uint256Hex(tokenId)
}
Expand All @@ -57,6 +71,10 @@ export function methodUriErc1155(id: bigint): string {
return URI_METHOD_ERC1155 + uint256Hex(id)
}

export function methodOwnerOfErc721(tokenId: bigint): string {
return OWNER_OF_METHOD_ERC721 + uint256Hex(tokenId)
}

export function supportsInterfaceMethodErc165(interfaceId: string): string {
return (
SUPPORTS_INTERFACE_METHOD_ERC165 +
Expand Down
12 changes: 9 additions & 3 deletions src/fetchers/ethers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from "../shared/decentraland-parcel"
import { moonCatsMetadata, isMoonCats } from "../shared/mooncats"
import { moonCatsCatId } from "./mooncats"
import { fetchStandardNftUrl } from "./standard-nft"
import { fetchStandardNftContractData } from "./standard-nft"

export default function ethersFetcher(
config: EthersFetcherConfig
Expand Down Expand Up @@ -50,9 +50,15 @@ export default function ethersFetcher(
return moonCatsMetadata(tokenId, moonCatsCatId(config))
}

return fetchMetadata(
await fetchStandardNftUrl(contractAddress, tokenId, config)
const [metadataUrl, owner] = await fetchStandardNftContractData(
contractAddress,
tokenId,
config
)

const metadata = await fetchMetadata(metadataUrl)

return { ...metadata, owner }
},
}
}
34 changes: 22 additions & 12 deletions src/fetchers/ethers/standard-nft.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,41 @@ import { normalizeTokenUrl, promiseAny } from "../../utils"
const ABI = [
// ERC-721
"function tokenURI(uint256 _tokenId) external view returns (string)",
"function ownerOf(uint256 _tokenId) external view returns (address)",
// ERC-1155
"function uri(uint256 _id) external view returns (string)",
// ERC-165
"function supportsInterface(bytes4 interfaceID) external view returns (bool)",
]

export async function fetchStandardNftUrl(
type NftContract = InstanceType<typeof Contract> & {
ownerOf: ContractFunction<string>
supportsInterface: ContractFunction<boolean>
tokenURI: ContractFunction<string>
uri: ContractFunction<string>
}

async function url(contract: NftContract, tokenId: string): Promise<string> {
const uri = await promiseAny([
contract.uri(tokenId),
contract.tokenURI(tokenId),
])
return normalizeTokenUrl(uri, tokenId)
}

export async function fetchStandardNftContractData(
contractAddress: Address,
tokenId: string,
config: EthersFetcherConfig
): Promise<string> {
): Promise<[string, Address]> {
const contract = new config.ethers.Contract(
contractAddress,
ABI,
config.provider
) as InstanceType<typeof Contract> & {
uri: ContractFunction<string>
tokenURI: ContractFunction<string>
supportsInterface: ContractFunction<boolean>
}
) as NftContract

const url = await promiseAny([
contract.uri(tokenId),
contract.tokenURI(tokenId),
return Promise.all([
url(contract, tokenId),
contract.ownerOf(tokenId).catch(() => ""),
])

return normalizeTokenUrl(url, tokenId)
}
3 changes: 2 additions & 1 deletion src/fetchers/shared/cryptokitties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ export async function cryptoKittiesMetadata(id: string): Promise<NftMetadata> {
image_url: string
}
return {
name: data?.name ?? "Unknown",
description: data?.bio ?? "−",
image: data?.image_url ?? "",
name: data?.name ?? "Unknown",
owner: "",
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/fetchers/shared/cryptopunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ const CRYPTOPUNKS_DESCRIPTION = `

export function cryptoPunksMetadata(index: string): NftMetadata {
return {
name: `CryptoPunk ${index}`,
description: CRYPTOPUNKS_DESCRIPTION,
image: `https://www.larvalabs.com/cryptopunks/cryptopunk${index}.png`,
name: `CryptoPunk ${index}`,
owner: "",
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/fetchers/shared/decentraland-estate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ export async function decentralandEstateMetadata(
const nft = data?.nfts?.[0]

return {
name: nft?.name ?? "Unknown",
description: nft?.estate?.data?.description ?? "−",
image: nft?.image ?? "",
name: nft?.name ?? "Unknown",
owner: nft?.owner?.address ?? "",
}
}

Expand Down
35 changes: 2 additions & 33 deletions src/fetchers/shared/decentraland-parcel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,44 +67,13 @@ export async function decentralandParcelMetadata(
const parcel = nft?.parcel

return {
name: nft?.name ?? `Parcel ${parcel?.x},${parcel?.y}`,
description: parcel?.data?.description ?? "−",
image: nft?.image ?? "",
name: nft?.name ?? `Parcel ${parcel?.x},${parcel?.y}`,
owner: nft?.owner?.address ?? "",
}
}

export function isDecentralandParcel(contractAddress: Address): boolean {
return addressesEqual(contractAddress, DECENTRALAND_PARCEL)
}

// query NFTByTokenId($contractAddress: String, $tokenId: String) {
// nfts(
// where: { contractAddress: $contractAddress, tokenId: $tokenId }
// first: 1
// ) {
// id
// name
// image
// contractAddress
// tokenId
// category
// owner {
// address
// }
// parcel {
// x
// y
// data {
// description
// }
// }
// estate {
// size
// data {
// description
// }
// }
// ens {
// subdomain
// }
// }
Loading

0 comments on commit a3ee65f

Please sign in to comment.