diff --git a/etc/sdk.api.md b/etc/sdk.api.md index ebf68f34e..5de2cd6df 100644 --- a/etc/sdk.api.md +++ b/etc/sdk.api.md @@ -694,7 +694,7 @@ export class ContractPublishedMetadata { // Warning: (ae-forgotten-export) The symbol "AbiFunction" needs to be exported by the entry point index.d.ts // // @public (undocumented) - extractFunctions(): AbiFunction[]; + extractFunctions(): Promise; // Warning: (ae-forgotten-export) The symbol "PublishedMetadata" needs to be exported by the entry point index.d.ts // // @public @@ -1843,7 +1843,7 @@ export function extractFunctions(predeployMetadataUri: string, storage: IStorage // Warning: (ae-internal-missing-underscore) The name "extractFunctionsFromAbi" should be prefixed with an underscore because the declaration is marked as @internal // // @internal (undocumented) -export function extractFunctionsFromAbi(abi: z.input): AbiFunction[]; +export function extractFunctionsFromAbi(abi: z.input, metadata?: Record): AbiFunction[]; // Warning: (ae-internal-missing-underscore) The name "fetchContractMetadataFromAddress" should be prefixed with an underscore because the declaration is marked as @internal // diff --git a/src/common/feature-detection.ts b/src/common/feature-detection.ts index 45c9d4456..0942fff00 100644 --- a/src/common/feature-detection.ts +++ b/src/common/feature-detection.ts @@ -5,6 +5,7 @@ import { AbiFunction, AbiSchema, AbiTypeSchema, + ContractInfoSchema, ContractSource, PreDeployMetadata, PreDeployMetadataFetched, @@ -91,13 +92,28 @@ export function extractConstructorParamsFromAbi( /** * @internal * @param abi + * @param metadata */ export function extractFunctionsFromAbi( abi: z.input, + metadata?: Record, ): AbiFunction[] { const functions = abi.filter((el) => el.type === "function"); + const parsed = []; for (const f of functions) { + const doc = + metadata?.output?.userdoc[ + Object.keys(metadata?.output?.userdoc.methods || {}).find( + (fn) => fn.substring(0, fn.indexOf("(")) === f.name, + ) || "" + ]?.notice || + metadata?.output?.devdoc.methods[ + Object.keys(metadata?.output?.devdoc.methods || {}).find( + (fn) => fn.substring(0, fn.indexOf("(")) === f.name, + ) || "" + ]?.details; + const args = f.inputs?.map((i) => `${i.name || "key"}: ${toJSType(i)}`)?.join(", ") || ""; @@ -111,6 +127,7 @@ export function extractFunctionsFromAbi( name: f.name ?? "unknown", signature, stateMutability: f.stateMutability ?? "", + comment: doc, }); } return parsed; @@ -265,10 +282,17 @@ async function fetchContractMetadata( const compilationTarget = metadata.settings.compilationTarget; const targets = Object.keys(compilationTarget); const name = compilationTarget[targets[0]]; + const info = ContractInfoSchema.parse({ + title: metadata.output.devdoc.title, + author: metadata.output.devdoc.author, + details: metadata.output.devdoc.detail, + notice: metadata.output.userdoc.notice, + }); return { name, abi, metadata, + info, }; } diff --git a/src/contracts/smart-contract.ts b/src/contracts/smart-contract.ts index c0fd4e3c6..f661405ae 100644 --- a/src/contracts/smart-contract.ts +++ b/src/contracts/smart-contract.ts @@ -19,10 +19,14 @@ import { IRoyalty, ThirdwebContract, } from "contracts"; -import { CustomContractSchema } from "../schema/contracts/custom"; +import { AbiSchema, CustomContractSchema } from "../schema/contracts/custom"; import { UpdateableNetwork } from "../core/interfaces/contract"; import { CallOverrides, ContractInterface } from "ethers"; -import { ALL_ROLES, detectContractFeature } from "../common"; +import { + ALL_ROLES, + detectContractFeature, + extractFunctionsFromAbi, +} from "../common"; import { ContractPlatformFee } from "../core/classes/contract-platform-fee"; import { ContractPublishedMetadata } from "../core/classes/contract-published-metadata"; import { BaseERC1155, BaseERC20, BaseERC721 } from "../types/eips"; @@ -190,7 +194,9 @@ export class SmartContract< // no-op } - const functions = this.publishedMetadata.extractFunctions(); + const functions = extractFunctionsFromAbi( + AbiSchema.parse(this.contractWrapper.abi), + ); const fn = functions.find((f) => f.name === functionName); if (!fn) { throw new Error( diff --git a/src/core/classes/contract-metadata.ts b/src/core/classes/contract-metadata.ts index 7a6c9f94e..3c52e40df 100644 --- a/src/core/classes/contract-metadata.ts +++ b/src/core/classes/contract-metadata.ts @@ -91,11 +91,15 @@ export class ContractMetadata< if (!data) { try { // try fetching metadata from bytecode - data = await fetchContractMetadataFromAddress( + const publishedMetadata = await fetchContractMetadataFromAddress( this.contractWrapper.readContract.address, this.contractWrapper.getProvider(), this.storage, ); + data = { + name: publishedMetadata.name, + description: publishedMetadata.info.title, + }; } catch (e) { // ignore } diff --git a/src/core/classes/contract-published-metadata.ts b/src/core/classes/contract-published-metadata.ts index f0a9553f9..ad32816e2 100644 --- a/src/core/classes/contract-published-metadata.ts +++ b/src/core/classes/contract-published-metadata.ts @@ -45,8 +45,17 @@ export class ContractPublishedMetadata { /** * @public */ - public extractFunctions(): AbiFunction[] { + public async extractFunctions(): Promise { + let publishedMetadata; + try { + publishedMetadata = await this.get(); + } catch (e) { + // ignore for built-in contracts + } // to construct a contract we already **have** to have the abi on the contract wrapper, so there is no reason to look fetch it again (means this function can become synchronous as well!) - return extractFunctionsFromAbi(AbiSchema.parse(this.contractWrapper.abi)); + return extractFunctionsFromAbi( + AbiSchema.parse(this.contractWrapper.abi), + publishedMetadata?.metadata, + ); } } diff --git a/src/schema/contracts/custom.ts b/src/schema/contracts/custom.ts index b47eff1ab..7bc5db7df 100644 --- a/src/schema/contracts/custom.ts +++ b/src/schema/contracts/custom.ts @@ -110,6 +110,16 @@ export const PublishedContractSchema = z.object({ metadataUri: z.string(), }); +/** + * @internal + */ +export const ContractInfoSchema = z.object({ + title: z.string().optional(), + author: z.string().optional(), + details: z.string().optional(), + notice: z.string().optional(), +}); + export type ContractParam = z.infer; export type PublishedContract = z.infer; export type AbiFunction = { @@ -127,4 +137,5 @@ export type PublishedMetadata = { name: string; abi: z.infer; metadata: Record; + info: z.infer; }; diff --git a/test/custom.test.ts b/test/custom.test.ts index ad567cbc2..80889f346 100644 --- a/test/custom.test.ts +++ b/test/custom.test.ts @@ -2,7 +2,6 @@ import { expectError, signers } from "./before-setup"; import { expect } from "chai"; import invariant from "tiny-invariant"; import { - DropERC721__factory, SignatureDrop__factory, TokenERC1155__factory, TokenERC20__factory, @@ -10,8 +9,7 @@ import { VoteERC20__factory, } from "contracts"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { uploadContractMetadata } from "./publisher.test"; -import { IpfsStorage, ThirdwebSDK } from "../src"; +import { ThirdwebSDK } from "../src"; import { ethers } from "ethers"; require("./before-setup"); @@ -28,14 +26,14 @@ describe("Custom Contracts", async () => { samWallet: SignerWithAddress, bobWallet: SignerWithAddress; let sdk: ThirdwebSDK; - let storage: IpfsStorage; let simpleContractUri: string; before(async () => { [adminWallet, samWallet, bobWallet] = signers; sdk = new ThirdwebSDK(adminWallet); - storage = new IpfsStorage(); - simpleContractUri = await uploadContractMetadata("Greeter", storage); + simpleContractUri = + "ipfs://QmNPcYsXDAZvQZXCG73WSjdiwffZkNkoJYwrDDtcgM142A/0"; + // if we update the test data - await uploadContractMetadata("Greeter", storage); }); beforeEach(async () => { @@ -131,7 +129,7 @@ describe("Custom Contracts", async () => { it("should extract functions", async () => { const c = await sdk.getContract(customContractAddress); invariant(c, "Contract undefined"); - const functions = c.publishedMetadata.extractFunctions(); + const functions = await c.publishedMetadata.extractFunctions(); expect(functions.length).gt(0); }); diff --git a/test/publisher.test.ts b/test/publisher.test.ts index 5b811eb4b..b3a68e1f9 100644 --- a/test/publisher.test.ts +++ b/test/publisher.test.ts @@ -36,17 +36,16 @@ describe("Publishing", async () => { let samWallet: SignerWithAddress; let bobWallet: SignerWithAddress; let sdk: ThirdwebSDK; - let storage: IpfsStorage; before("Upload abis", async () => { [adminWallet, samWallet, bobWallet] = signers; + console.log("beforeeee"); sdk = new ThirdwebSDK(adminWallet); - storage = new IpfsStorage(); - simpleContractUri = await uploadContractMetadata("Greeter", storage); - contructorParamsContractUri = await uploadContractMetadata( - "ConstructorParams", - storage, - ); + simpleContractUri = + "ipfs://QmNPcYsXDAZvQZXCG73WSjdiwffZkNkoJYwrDDtcgM142A/0"; + // if we change the test data - await uploadContractMetadata("Greeter", storage); + contructorParamsContractUri = + "ipfs://QmT5Dx3xigHr6BPG8scxbX7JaAucHRD9UPXc6FCtgcNn5e/0"; }); beforeEach(async () => { @@ -165,8 +164,7 @@ describe("Publishing", async () => { }); it("AzukiWithMinting mintable", async () => { - const realSDK = new ThirdwebSDK(adminWallet); - const pub = await realSDK.getPublisher(); + const pub = await sdk.getPublisher(); const ipfsUri = "ipfs://QmPPPoKk2mwoxBVTW5qMMNwaV4Ja5qDoq7fFZNFFvr3YsW/1"; const tx = await pub.publish(ipfsUri); const contract = await tx.data(); @@ -175,7 +173,7 @@ describe("Publishing", async () => { contract.id, [10, "bar"], ); - const c = await realSDK.getContract(deployedAddr); + const c = await sdk.getContract(deployedAddr); invariant(c.nft, "no nft detected"); invariant(c.nft.mint, "no minter detected"); const tx2 = await c.nft.mint.to(adminWallet.address, { @@ -188,6 +186,11 @@ describe("Publishing", async () => { const all = await c.nft.query.all(); expect(all.length).to.eq(1); invariant(c.royalties, "no royalties detected"); + const prevMeta = await c.metadata.get(); + expect(prevMeta.name).to.eq("CustomAzukiContract"); + expect(prevMeta.description).to.eq( + "Azuki contract that can be fully used in the thirdweb dashboard", + ); await c.metadata.set({ name: "Hello", }); @@ -196,11 +199,10 @@ describe("Publishing", async () => { }); it("ERC721Dropable feature detection", async () => { - const realSDK = new ThirdwebSDK(adminWallet); - const pub = realSDK.getPublisher(); + const pub = sdk.getPublisher(); const ipfsUri = "ipfs://QmWaidQMSYHPzYYZCxMc2nSk2vrD28mS43Xc9k7QFyAGja/0"; const addr = await pub.deployContract(ipfsUri, []); - const c = await realSDK.getContract(addr); + const c = await sdk.getContract(addr); invariant(c.nft, "nft must be defined"); invariant(c.nft.drop, "drop must be defined"); @@ -225,8 +227,7 @@ describe("Publishing", async () => { }); it("Constructor params with tuples", async () => { - const realSDK = new ThirdwebSDK(adminWallet); - const pub = await realSDK.getPublisher(); + const pub = await sdk.getPublisher(); const ipfsUri = "ipfs://QmZQa56Cj1gFnZgKSkvGE5uzhaQrQV3nU6upDWDusCaCwY/0"; const addr = await pub.deployContract(ipfsUri, [ "0x1234",