thirdweb 0.2.8 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,49 +1,3 @@
1
- /**
2
- * Error that may get thrown if IPFS returns nothing for a given uri.
3
- * @internal
4
- */
5
- export class NotFoundError extends Error {
6
- /** @internal */
7
- constructor(identifier?: string) {
8
- super(identifier ? `Object with id ${identifier} NOT FOUND` : "NOT_FOUND");
9
- }
10
- }
11
-
12
- /**
13
- * Error that may get thrown if an invalid address was passed
14
- * @internal
15
- */
16
- export class InvalidAddressError extends Error {
17
- /** @internal */
18
- constructor(address?: string) {
19
- super(
20
- address ? `'${address}' is an invalid address` : "Invalid address passed"
21
- );
22
- }
23
- }
24
-
25
- /**
26
- * @internal
27
- */
28
- export class MissingRoleError extends Error {
29
- /** @internal */
30
- /** @internal */
31
- constructor(address: string, role: string) {
32
- super(`MISSING ROLE: ${address} does not have the '${role}' role`);
33
- }
34
- }
35
-
36
- /**
37
- * @internal
38
- */
39
- export class AssetNotFoundError extends Error {
40
- /** @internal */
41
- /** @internal */
42
- constructor(message = "The asset you're trying to use could not be found.") {
43
- super(`message: ${message}`);
44
- }
45
- }
46
-
47
1
  /**
48
2
  * @internal
49
3
  */
@@ -54,16 +8,6 @@ export class UploadError extends Error {
54
8
  }
55
9
  }
56
10
 
57
- /**
58
- * @internal
59
- */
60
- export class FileNameMissingError extends Error {
61
- /** @internal */
62
- constructor() {
63
- super("File name is required when object is not a `File` type object.");
64
- }
65
- }
66
-
67
11
  /**
68
12
  * @internal
69
13
  */
@@ -71,43 +15,11 @@ export class DuplicateFileNameError extends Error {
71
15
  /** @internal */
72
16
  constructor(fileName: string) {
73
17
  super(
74
- `DUPLICATE_FILE_NAME_ERROR: File name ${fileName} was passed for more than one file.`
18
+ `DUPLICATE_FILE_NAME_ERROR: File name ${fileName} was passed for more than one file.`,
75
19
  );
76
20
  }
77
21
  }
78
22
 
79
- /**
80
- * @internal
81
- */
82
- export class NotEnoughTokensError extends Error {
83
- /** @internal */
84
- constructor(contractAddress: string, quantity: number, available: number) {
85
- super(
86
- `BALANCE ERROR: you do not have enough balance on contract ${contractAddress} to use ${quantity} tokens. You have ${available} tokens available.`
87
- );
88
- }
89
- }
90
-
91
- /**
92
- * @internal
93
- */
94
- export class MissingOwnerRoleError extends Error {
95
- /** @internal */
96
- constructor() {
97
- super(`LIST ERROR: you should be the owner of the token to list it.`);
98
- }
99
- }
100
-
101
- /**
102
- * @internal
103
- */
104
- export class QuantityAboveLimitError extends Error {
105
- /** @internal */
106
- constructor(quantity: string) {
107
- super(`BUY ERROR: You cannot buy more than ${quantity} tokens`);
108
- }
109
- }
110
-
111
23
  /**
112
24
  * Thrown when data fails to fetch from storage.
113
25
  * @internal
@@ -121,105 +33,3 @@ export class FetchError extends Error {
121
33
  this.innerError = innerError;
122
34
  }
123
35
  }
124
-
125
- /**
126
- * Thrown when attempting to create a snapshot with duplicate leafs
127
- * @internal
128
- */
129
- export class DuplicateLeafsError extends Error {
130
- constructor(message?: string) {
131
- super(`DUPLICATE_LEAFS${message ? ` : ${message}` : ""}`);
132
- }
133
- }
134
-
135
- /**
136
- * Thrown when attempting to update/cancel an auction that already started
137
- * @internal
138
- */
139
- export class AuctionAlreadyStartedError extends Error {
140
- constructor(id?: string) {
141
- super(
142
- `Auction already started with existing bid${id ? `, id: ${id}` : ""}`
143
- );
144
- }
145
- }
146
-
147
- /**
148
- * @internal
149
- */
150
- export class FunctionDeprecatedError extends Error {
151
- /** @internal */
152
- constructor(message: string) {
153
- super(`FUNCTION DEPRECATED. ${message ? `Use ${message} instead` : ""}`);
154
- }
155
- }
156
- /**
157
- * Thrown when trying to retrieve a listing from a marketplace that doesn't exist
158
- * @internal
159
- */
160
- export class ListingNotFoundError extends Error {
161
- constructor(marketplaceContractAddress: string, listingId?: string) {
162
- super(
163
- `Could not find listing.${
164
- marketplaceContractAddress
165
- ? ` marketplace address: ${marketplaceContractAddress}`
166
- : ""
167
- }${listingId ? ` listing id: ${listingId}` : ""}`
168
- );
169
- }
170
- }
171
-
172
- /**
173
- * Thrown when trying to retrieve a listing of the wrong type
174
- * @internal
175
- */
176
- export class WrongListingTypeError extends Error {
177
- constructor(
178
- marketplaceContractAddress: string,
179
- listingId?: string,
180
- actualType?: string,
181
- expectedType?: string
182
- ) {
183
- super(
184
- `Incorrect listing type. Are you sure you're using the right method?.${
185
- marketplaceContractAddress
186
- ? ` marketplace address: ${marketplaceContractAddress}`
187
- : ""
188
- }${listingId ? ` listing id: ${listingId}` : ""}${
189
- expectedType ? ` expected type: ${expectedType}` : ""
190
- }${actualType ? ` actual type: ${actualType}` : ""}`
191
- );
192
- }
193
- }
194
-
195
- /**
196
- * Thrown when attempting to transfer an asset that has restricted transferability
197
- * @internal
198
- */
199
- export class RestrictedTransferError extends Error {
200
- constructor(assetAddress?: string) {
201
- super(
202
- `Failed to transfer asset, transfer is restricted.${
203
- assetAddress ? ` Address : ${assetAddress}` : ""
204
- }`
205
- );
206
- }
207
- }
208
-
209
- /**
210
- * Thrown when attempting to execute an admin-role function.
211
- * @internal
212
- */
213
- export class AdminRoleMissingError extends Error {
214
- constructor(
215
- address?: string,
216
- contractAddress?: string,
217
- message = "Failed to execute transaction"
218
- ) {
219
- super(
220
- `${message}, admin role is missing${
221
- address ? ` on address: ${address}` : ""
222
- }${contractAddress ? ` on contract: ${contractAddress}` : ""}`
223
- );
224
- }
225
- }
@@ -0,0 +1,66 @@
1
+ import { logger } from "../helpers/logger";
2
+ import { CompileOptions } from "../interfaces/Builder";
3
+ import { ContractPayload } from "../interfaces/ContractPayload";
4
+ import { BaseBuilder } from "./builder-base";
5
+ import { execSync } from "child_process";
6
+ import { existsSync, readFileSync, rmdirSync } from "fs";
7
+ import { basename, join } from "path";
8
+ import { parse } from "yaml";
9
+
10
+ export class BrownieBuilder extends BaseBuilder {
11
+ public async compile(options: CompileOptions): Promise<{
12
+ contracts: ContractPayload[];
13
+ }> {
14
+ const config = parse(
15
+ readFileSync(join(options.projectPath, "brownie-config.yaml"), "utf-8"),
16
+ );
17
+
18
+ const buildPath = join(
19
+ options.projectPath,
20
+ config?.project_structure?.build || "./build",
21
+ );
22
+
23
+ if (options.clean) {
24
+ logger.info("Cleaning build directory");
25
+ existsSync(buildPath) && rmdirSync(buildPath, { recursive: true });
26
+ }
27
+
28
+ logger.info("Compiling...");
29
+ execSync("brownie compile");
30
+
31
+ const contractsPath = join(buildPath, "contracts/");
32
+
33
+ const contracts: ContractPayload[] = [];
34
+ const files: string[] = [];
35
+ this.findFiles(contractsPath, /^.*(?<!dbg)\.json$/, files);
36
+
37
+ for (const file of files) {
38
+ logger.debug("Processing:", file.replace(contractsPath, ""));
39
+ const contractName = basename(file, ".json");
40
+ const contractJsonFile = readFileSync(file, "utf-8");
41
+
42
+ const contractInfo = JSON.parse(contractJsonFile);
43
+ const abi = contractInfo.abi;
44
+ const bytecode = contractInfo.bytecode;
45
+
46
+ for (const input of abi) {
47
+ if (this.isThirdwebContract(input)) {
48
+ if (contracts.find((c) => c.name === contractName)) {
49
+ logger.error(
50
+ `Found multiple contracts with name "${contractName}". Contract names should be unique.`,
51
+ );
52
+ process.exit(1);
53
+ }
54
+ contracts.push({
55
+ abi,
56
+ bytecode,
57
+ name: contractName,
58
+ });
59
+ break;
60
+ }
61
+ }
62
+ }
63
+
64
+ return { contracts };
65
+ }
66
+ }
@@ -1,12 +1,14 @@
1
1
  import { ContractPayload } from "../interfaces/ContractPayload";
2
2
  import { ProjectType } from "./../types/ProjectType";
3
+ import { BrownieBuilder } from "./brownie";
3
4
  import { FoundryBuilder } from "./foundry";
4
5
  import { HardhatBuilder } from "./hardhat";
6
+ import { TruffleBuilder } from "./truffle";
5
7
 
6
8
  export default async function build(
7
9
  path: string,
8
10
  projectType: ProjectType,
9
- clean: boolean
11
+ clean: boolean,
10
12
  ): Promise<{
11
13
  contracts: ContractPayload[];
12
14
  }> {
@@ -29,6 +31,24 @@ export default async function build(
29
31
  });
30
32
  }
31
33
 
34
+ case "truffle": {
35
+ const builder = new TruffleBuilder();
36
+ return await builder.compile({
37
+ name: "",
38
+ projectPath: path,
39
+ clean,
40
+ });
41
+ }
42
+
43
+ case "brownie": {
44
+ const builder = new BrownieBuilder();
45
+ return await builder.compile({
46
+ name: "",
47
+ projectPath: path,
48
+ clean,
49
+ });
50
+ }
51
+
32
52
  default: {
33
53
  throw new Error("Unknown project type");
34
54
  }
@@ -1,11 +1,12 @@
1
- import { existsSync, readdirSync, statSync } from "fs";
2
- import { basename, join } from "path";
1
+ import { logger } from "../helpers/logger";
3
2
  import { CompileOptions, IBuilder } from "../interfaces/Builder";
4
3
  import { ContractPayload } from "../interfaces/ContractPayload";
4
+ import { existsSync, readdirSync, statSync } from "fs";
5
+ import { basename, join } from "path";
5
6
 
6
7
  export abstract class BaseBuilder implements IBuilder {
7
8
  abstract compile(
8
- options: CompileOptions
9
+ options: CompileOptions,
9
10
  ): Promise<{ contracts: ContractPayload[] }>;
10
11
 
11
12
  protected isThirdwebContract(input: any): boolean {
@@ -25,14 +26,20 @@ export abstract class BaseBuilder implements IBuilder {
25
26
  return;
26
27
  }
27
28
 
28
- var files = readdirSync(startPath);
29
- for (var i = 0; i < files.length; i++) {
30
- var filename = join(startPath, files[i]);
29
+ const files = readdirSync(startPath);
30
+ for (let i = 0; i < files.length; i++) {
31
+ const filename = join(startPath, files[i]);
31
32
  //skip the actual thirdweb contract itself
32
33
  if (basename(filename, ".json") === "ThirdwebContract") {
33
34
  continue;
34
35
  }
35
- var stat = statSync(filename);
36
+ const stat = statSync(filename);
37
+
38
+ // brownie has a "depdendencies" directory *inside* the build directory, if we detect that we should skip it
39
+ if (stat.isDirectory() && basename(filename) === "dependencies") {
40
+ logger.debug('skipping "dependencies" directory');
41
+ continue;
42
+ }
36
43
  if (stat.isDirectory()) {
37
44
  this.findFiles(filename, filter, results);
38
45
  } else if (filter.test(filename)) {
@@ -1,11 +1,10 @@
1
- import { execSync } from "child_process";
2
- import { readFileSync } from "fs";
3
- import { basename, join } from "path";
4
1
  import { logger } from "../helpers/logger";
5
2
  import { CompileOptions } from "../interfaces/Builder";
6
3
  import { ContractPayload } from "../interfaces/ContractPayload";
7
-
8
4
  import { BaseBuilder } from "./builder-base";
5
+ import { execSync } from "child_process";
6
+ import { readFileSync } from "fs";
7
+ import { basename, join } from "path";
9
8
 
10
9
  export class FoundryBuilder extends BaseBuilder {
11
10
  public async compile(options: CompileOptions): Promise<{
@@ -44,7 +43,7 @@ export class FoundryBuilder extends BaseBuilder {
44
43
  if (this.isThirdwebContract(input)) {
45
44
  if (contracts.find((c) => c.name === contractName)) {
46
45
  logger.error(
47
- `Found multiple contracts with name "${contractName}". Contract names should be unique.`
46
+ `Found multiple contracts with name "${contractName}". Contract names should be unique.`,
48
47
  );
49
48
  process.exit(1);
50
49
  }
@@ -58,11 +57,6 @@ export class FoundryBuilder extends BaseBuilder {
58
57
  }
59
58
  }
60
59
 
61
- logger.info(
62
- "Detected thirdweb contracts:",
63
- contracts.map((c) => c.name).join(", ")
64
- );
65
-
66
60
  return { contracts };
67
61
  }
68
62
  }
@@ -1,11 +1,11 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from "fs";
2
- import { HardhatConfig } from "hardhat/types";
3
- import { basename, join, resolve } from "path";
4
1
  import { logger } from "../helpers/logger";
5
2
  import { CompileOptions } from "../interfaces/Builder";
6
3
  import { ContractPayload } from "../interfaces/ContractPayload";
7
- import { execSync } from "child_process";
8
4
  import { BaseBuilder } from "./builder-base";
5
+ import { execSync } from "child_process";
6
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
7
+ import { HardhatConfig } from "hardhat/types";
8
+ import { basename, join, resolve } from "path";
9
9
 
10
10
  export class HardhatBuilder extends BaseBuilder {
11
11
  public async compile(options: CompileOptions): Promise<{
@@ -23,25 +23,25 @@ export class HardhatBuilder extends BaseBuilder {
23
23
  // then we look up the hardhat config extractor file path from there
24
24
  const configExtractorScriptPath = resolve(
25
25
  __dirname,
26
- "../helpers/hardhat-config-extractor.js"
26
+ "../helpers/hardhat-config-extractor.js",
27
27
  );
28
28
 
29
29
  //the hardhat extractor **logs out** the runtime config of hardhat, we take that stdout and parse it
30
30
  const stringifiedConfig = execSync(
31
- `npx hardhat run ${configExtractorScriptPath} --no-compile`
31
+ `npx hardhat run ${configExtractorScriptPath} --no-compile`,
32
32
  ).toString();
33
33
  //voila the hardhat config
34
34
  const actualHardhatConfig = JSON.parse(stringifiedConfig) as HardhatConfig;
35
35
 
36
36
  logger.debug(
37
37
  "successfully extracted hardhat config",
38
- actualHardhatConfig.paths
38
+ actualHardhatConfig.paths,
39
39
  );
40
40
 
41
41
  const artifactsPath = actualHardhatConfig.paths.artifacts;
42
42
  const sourcesDir = actualHardhatConfig.paths.sources.replace(
43
43
  options.projectPath,
44
- ""
44
+ "",
45
45
  );
46
46
  const contractsPath = join(artifactsPath, sourcesDir);
47
47
 
@@ -62,7 +62,7 @@ export class HardhatBuilder extends BaseBuilder {
62
62
  if (this.isThirdwebContract(input)) {
63
63
  if (contracts.find((c) => c.name === contractName)) {
64
64
  logger.error(
65
- `Found multiple contracts with name "${contractName}". Contract names should be unique.`
65
+ `Found multiple contracts with name "${contractName}". Contract names should be unique.`,
66
66
  );
67
67
  process.exit(1);
68
68
  }
@@ -76,11 +76,6 @@ export class HardhatBuilder extends BaseBuilder {
76
76
  }
77
77
  }
78
78
 
79
- logger.info(
80
- "Detected thirdweb contracts:",
81
- contracts.map((c) => c.name).join(", ")
82
- );
83
-
84
79
  return {
85
80
  contracts,
86
81
  };
@@ -0,0 +1,65 @@
1
+ import { logger } from "../helpers/logger";
2
+ import { CompileOptions } from "../interfaces/Builder";
3
+ import { ContractPayload } from "../interfaces/ContractPayload";
4
+ import { BaseBuilder } from "./builder-base";
5
+ import { execSync } from "child_process";
6
+ import { existsSync, readFileSync, rmdirSync } from "fs";
7
+ import { basename, join } from "path";
8
+
9
+ export class TruffleBuilder extends BaseBuilder {
10
+ public async compile(options: CompileOptions): Promise<{
11
+ contracts: ContractPayload[];
12
+ }> {
13
+ // get the current config first
14
+ const truffleConfig = require(join(
15
+ options.projectPath,
16
+ "truffle-config.js",
17
+ ));
18
+
19
+ const buildPath = join(
20
+ options.projectPath,
21
+ truffleConfig.contracts_build_directory || "./build/contracts",
22
+ );
23
+
24
+ if (options.clean) {
25
+ logger.info("Cleaning build directory");
26
+ existsSync(buildPath) && rmdirSync(buildPath, { recursive: true });
27
+ }
28
+
29
+ logger.info("Compiling...");
30
+ execSync("npx truffle compile");
31
+
32
+ const contracts: ContractPayload[] = [];
33
+ const files: string[] = [];
34
+ this.findFiles(buildPath, /^.*(?<!dbg)\.json$/, files);
35
+
36
+ for (const file of files) {
37
+ logger.debug("Processing:", file.replace(buildPath, ""));
38
+ const contractName = basename(file, ".json");
39
+ const contractJsonFile = readFileSync(file, "utf-8");
40
+
41
+ const contractInfo = JSON.parse(contractJsonFile);
42
+ const abi = contractInfo.abi;
43
+ const bytecode = contractInfo.bytecode;
44
+
45
+ for (const input of abi) {
46
+ if (this.isThirdwebContract(input)) {
47
+ if (contracts.find((c) => c.name === contractName)) {
48
+ logger.error(
49
+ `Found multiple contracts with name "${contractName}". Contract names should be unique.`,
50
+ );
51
+ process.exit(1);
52
+ }
53
+ contracts.push({
54
+ abi,
55
+ bytecode,
56
+ name: contractName,
57
+ });
58
+ break;
59
+ }
60
+ }
61
+ }
62
+
63
+ return { contracts };
64
+ }
65
+ }
@@ -0,0 +1,13 @@
1
+ import { logger } from "../helpers/logger";
2
+ import { ProjectType } from "../types/ProjectType";
3
+ import { Detector } from "./detector";
4
+ import { existsSync } from "fs";
5
+
6
+ export default class BrownieDetector implements Detector {
7
+ public projectType: ProjectType = "brownie";
8
+
9
+ public matches(path: string): boolean {
10
+ logger.debug("Checking if " + path + " is a Foundry project");
11
+ return existsSync(path + "/brownie-config.yaml");
12
+ }
13
+ }
@@ -1,16 +1,46 @@
1
+ import { logger } from "../helpers/logger";
1
2
  import { ProjectType } from "../types/ProjectType";
3
+ import BrownieDetector from "./brownie";
2
4
  import { Detector } from "./detector";
3
5
  import FoundryDetector from "./foundry";
4
6
  import HardhatDetector from "./hardhat";
7
+ import TruffleDetector from "./truffle";
8
+ import inquirer from "inquirer";
5
9
 
6
10
  export default async function detect(path: string): Promise<ProjectType> {
7
- const detectors: Detector[] = [new HardhatDetector(), new FoundryDetector()];
11
+ const detectors: Detector[] = [
12
+ new HardhatDetector(),
13
+ new FoundryDetector(),
14
+ new TruffleDetector(),
15
+ new BrownieDetector(),
16
+ ];
8
17
 
9
- for (const detector of detectors) {
10
- if (await detector.matches(path)) {
11
- return detector.projectType;
12
- }
18
+ const possibleProjectTypes = detectors
19
+ .filter((detector) => detector.matches(path))
20
+ .map((detector) => detector.projectType);
21
+
22
+ //if there is no project returned at all then just return unknown}
23
+ if (!possibleProjectTypes.length) {
24
+ return "unknown";
25
+ }
26
+ //if there is only one possible option just return it
27
+ if (possibleProjectTypes.length === 1) {
28
+ logger.info("Detected project type:", possibleProjectTypes[0]);
29
+ return possibleProjectTypes[0];
13
30
  }
14
31
 
15
- return "unknown";
32
+ logger.info(
33
+ "Detected multiple possible build tools:",
34
+ possibleProjectTypes.map((s) => `"${s}"`).join(", "),
35
+ );
36
+
37
+ const question = "How would you like to compile your contracts";
38
+
39
+ const answer = await inquirer.prompt({
40
+ type: "list",
41
+ choices: possibleProjectTypes,
42
+ name: question,
43
+ });
44
+
45
+ return answer[question];
16
46
  }
@@ -3,5 +3,5 @@ import { ProjectType } from "../types/ProjectType";
3
3
  export interface Detector {
4
4
  projectType: ProjectType;
5
5
 
6
- matches(path: string): Promise<boolean>;
6
+ matches(path: string): boolean;
7
7
  }
@@ -1,12 +1,12 @@
1
- import { existsSync } from "fs";
2
1
  import { logger } from "../helpers/logger";
3
2
  import { ProjectType } from "../types/ProjectType";
4
3
  import { Detector } from "./detector";
4
+ import { existsSync } from "fs";
5
5
 
6
6
  export default class FoundryDetector implements Detector {
7
7
  public projectType: ProjectType = "foundry";
8
8
 
9
- public async matches(path: string): Promise<boolean> {
9
+ public matches(path: string): boolean {
10
10
  logger.debug("Checking if " + path + " is a Foundry project");
11
11
  return existsSync(path + "/foundry.toml");
12
12
  }
@@ -1,12 +1,12 @@
1
- import { existsSync } from "fs";
2
1
  import { logger } from "../helpers/logger";
3
2
  import { ProjectType } from "../types/ProjectType";
4
3
  import { Detector } from "./detector";
4
+ import { existsSync } from "fs";
5
5
 
6
6
  export default class HardhatDetector implements Detector {
7
7
  public projectType: ProjectType = "hardhat";
8
8
 
9
- public async matches(path: string): Promise<boolean> {
9
+ public matches(path: string): boolean {
10
10
  logger.debug("Checking if " + path + " is a Hardhat project");
11
11
  return (
12
12
  existsSync(path + "/hardhat.config.js") ||
@@ -0,0 +1,13 @@
1
+ import { logger } from "../helpers/logger";
2
+ import { ProjectType } from "../types/ProjectType";
3
+ import { Detector } from "./detector";
4
+ import { existsSync } from "fs";
5
+
6
+ export default class TruffleDetector implements Detector {
7
+ public projectType: ProjectType = "truffle" as const;
8
+
9
+ public matches(path: string): boolean {
10
+ logger.debug("Checking if " + path + " is a Truffle project");
11
+ return existsSync(path + "/truffle-config.js");
12
+ }
13
+ }
@@ -14,7 +14,7 @@ import { Json } from "../types";
14
14
  */
15
15
  export function replaceFilePropertiesWithHashes(
16
16
  object: Record<string, any>,
17
- cids: string[]
17
+ cids: string[],
18
18
  ) {
19
19
  const keys = Object.keys(object);
20
20
  for (const key in keys) {
@@ -43,7 +43,7 @@ export function replaceFilePropertiesWithHashes(
43
43
  export function replaceHashWithGatewayUrl(
44
44
  object: Record<string, any>,
45
45
  scheme: string,
46
- gatewayUrl: string
46
+ gatewayUrl: string,
47
47
  ): Record<string, any> {
48
48
  const keys = Object.keys(object);
49
49
  for (const key in keys) {
@@ -75,7 +75,7 @@ export function replaceHashWithGatewayUrl(
75
75
  export function resolveGatewayUrl<T extends Json>(
76
76
  ipfsHash: T,
77
77
  scheme: string,
78
- gatewayUrl: string
78
+ gatewayUrl: string,
79
79
  ): T {
80
80
  if (typeof ipfsHash === "string") {
81
81
  return ipfsHash && ipfsHash.toLowerCase().includes(scheme)