Compare commits

...
Author SHA1 Message Date
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
cbf5508bd5 Version Packages (#449)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-12-07 14:21:51 +13:00
Joaquim VergesandGitHub 3731459d33 [SDK] Less strict prebuilt checks to support OSRF variants (#451) 2022-12-07 13:08:12 +13:00
Adam MajmudarandGitHub 64f24f1bb1 CLI auto-updates on every run (#441) 2022-12-06 02:40:31 +00:00
Joaquim VergesandGitHub cac6c30bca [SDK] Fix setting allowance or approvals gaslessly (#450) 2022-12-06 11:49:04 +13:00
Adam MajmudarandGitHub e7740a59db Detect if forge isn't installed and display error (#447) 2022-12-05 16:54:13 -05:00
Jonas DanielsandGitHub 7a37e564fd [SDK, React] - useContract fail fast (#448) 2022-12-05 21:41:58 +00:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
e173636f4b Version Packages (#446)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-12-04 11:34:00 +13:00
Joaquim VergesandGitHub 8c6cdaa288 [SDK] Fix creating listings with a magic.link wallet (#443) 2022-12-03 21:58:44 +00:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
ad9b3febff Version Packages (#445)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-12-04 07:49:38 +13:00
Adam MajmudarandGitHub 94f49b1294 Move framework check back to template (#444) 2022-12-03 18:40:46 +00:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
517e2a75e3 Version Packages (#442)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-12-02 16:26:33 -08:00
Jonas DanielsandGitHub 72692689e9 initialize pay package (#433) 2022-12-02 22:52:04 +00:00
Jonas DanielsandGitHub efc56fa580 [SDK] - improved batch provider (#440) 2022-12-02 22:39:11 +00:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
564b17e760 Version Packages (#419)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-12-03 11:25:35 +13:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b4507db963 Bump decode-uri-component from 0.2.0 to 0.2.2 (#439)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-02 14:10:31 -08:00
Joaquim VergesandGitHub f451da6395 [SDK] Fix SigMint detection for OSRF NFT Collection contracts (#438) 2022-12-03 11:00:21 +13:00
Jonas DanielsandGitHub ee47840767 fix #429 (#436) 2022-12-02 10:47:46 -08:00
Adam MajmudarandGitHub bef97f72f4 [CLI] Make solc default backup compilation (#435) 2022-12-02 17:11:20 +00:00
Adam MajmudarandGitHub 3e87dd3ccb [CLI] display file names when there are duplicate contract names (#432) 2022-12-02 03:21:40 +00:00
Joaquim VergesandGitHub def42511ff [SOL] Remove workaround for getLargestAccount (#434) 2022-12-02 02:42:26 +00:00
Adam MajmudarandGitHub c42545a647 [CLI] fix CLI on paths with spaces for windows (#431) 2022-12-01 19:03:54 -05:00
Adam MajmudarandGitHub 6e20d1f21a [CLI] allow adding contracts to existing projects (#428) 2022-12-01 20:31:06 +00:00
Joaquim VergesandGitHub e684d7e1ca [CLI] Fix metadata upload with special characters (#430) 2022-12-01 20:14:27 +00:00
Adam MajmudarandGitHub f545a67e9f [Auth] include credentials when requesting user (#425) 2022-12-01 18:17:31 +00:00
Jake LooandGitHub 034a257442 [SDK] Support more types of Minimal Proxy (#429) 2022-12-01 20:30:09 +13:00
1bfb91dbf3 [SDK] Improve error parsing for other wallets (#424)
Co-authored-by: Jonas Daniels <[email protected]>
2022-11-30 18:02:21 -08:00
Jonas DanielsandGitHub 5d95cbcdb2 fix turbo (#427) 2022-12-01 00:52:06 +00:00
Jonas DanielsandGitHub 639e535ed5 init unity-js-bridge package (#420) 2022-11-30 22:36:16 +00:00
af3acc6b10 [SDK] Add support for AWS KMS (#416)
Co-authored-by: Jonas Daniels <[email protected]>
2022-11-28 20:34:24 -08:00
Joaquim VergesandGitHub eca47763cd [SDK] increase gas limit for opening packs (#418) 2022-11-29 03:30:15 +00:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
37b5bec897 Version Packages (#417)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-11-28 18:23:23 -08:00
Nacho IacovinoandGitHub 48d91e4431 [CLI] Fix mappings for prebuilts (#415) 2022-11-28 18:13:00 -08:00
108 changed files with 5055 additions and 1297 deletions
+12
View File
@@ -1,5 +1,17 @@
# @thirdweb-dev/auth
## 2.0.37
## 2.0.36
## 2.0.35
## 2.0.34
### Patch Changes
- [#425](https://github.com/thirdweb-dev/js/pull/425) [`f545a67`](https://github.com/thirdweb-dev/js/commit/f545a67e9fb597d27bb39ca515d24d306fbb121a) Thanks [@adam-maj](https://github.com/adam-maj)! - Add sameSite none and include credentials
## 2.0.33
## 2.0.32
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/auth",
"version": "2.0.33",
"version": "2.0.37",
"exports": {
"./next": {
"module": "./next/dist/thirdweb-dev-auth-next.esm.js",
@@ -43,7 +43,7 @@ export default async function handler(
path: "/",
httpOnly: true,
secure: true,
sameSite: "strict",
sameSite: "none",
}),
);
+2 -2
View File
@@ -40,11 +40,11 @@ export function ThirdwebNextAuth(cfg: ThirdwebNextAuthConfig) {
path: "/",
httpOnly: true,
secure: true,
sameSite: "strict",
sameSite: "none",
}),
);
return { address };
return { id: address, address };
} catch (err) {
return null;
}
+1 -1
View File
@@ -47,7 +47,7 @@ export default async function handler(
path: "/",
httpOnly: true,
secure: true,
sameSite: "strict",
sameSite: "none",
}),
);
@@ -47,7 +47,7 @@ export default async function handler(
path: "/",
httpOnly: true,
secure: true,
sameSite: "strict",
sameSite: "none",
}),
);
+54
View File
@@ -1,5 +1,59 @@
# thirdweb
## 0.9.6
### Patch Changes
- [#441](https://github.com/thirdweb-dev/js/pull/441) [`64f24f1`](https://github.com/thirdweb-dev/js/commit/64f24f1bb167c3d0cb3beca51fd300eaec21cc27) Thanks [@adam-maj](https://github.com/adam-maj)! - CLI autoupdates on run
- [#447](https://github.com/thirdweb-dev/js/pull/447) [`e7740a5`](https://github.com/thirdweb-dev/js/commit/e7740a59db022a3396194ac66cd195d8ae754d57) Thanks [@adam-maj](https://github.com/adam-maj)! - Detect whether forge is installed
- Updated dependencies [[`3731459`](https://github.com/thirdweb-dev/js/commit/3731459d33f1ded7ebb69124809449b901b3ad3d), [`cac6c30`](https://github.com/thirdweb-dev/js/commit/cac6c30bca5e17df81d746ef81316af47d5e252e), [`7a37e56`](https://github.com/thirdweb-dev/js/commit/7a37e564fd5d5a9df84c8da44ecaf6c42f67a0e2)]:
- @thirdweb-dev/sdk@3.6.7
## 0.9.5
### Patch Changes
- Updated dependencies [[`8c6cdaa`](https://github.com/thirdweb-dev/js/commit/8c6cdaa2887fb2cc40d3ee6991233d195d103805)]:
- @thirdweb-dev/sdk@3.6.6
## 0.9.4
### Patch Changes
- [#444](https://github.com/thirdweb-dev/js/pull/444) [`94f49b1`](https://github.com/thirdweb-dev/js/commit/94f49b1294bc0208171b16ed413cbb44644af017) Thanks [@adam-maj](https://github.com/adam-maj)! - Add changeset
## 0.9.3
### Patch Changes
- Updated dependencies [[`efc56fa`](https://github.com/thirdweb-dev/js/commit/efc56fa5802490ac8ef50037658d046afd89e9a1)]:
- @thirdweb-dev/sdk@3.6.5
## 0.9.2
### Patch Changes
- [#430](https://github.com/thirdweb-dev/js/pull/430) [`e684d7e`](https://github.com/thirdweb-dev/js/commit/e684d7e1ca16f633bce8698529d16bf60c264aa4) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix uploading contracts with special characters in the comments
- [#435](https://github.com/thirdweb-dev/js/pull/435) [`bef97f7`](https://github.com/thirdweb-dev/js/commit/bef97f72f4860596b7b70b05e67fcadf2c9a012e) Thanks [@adam-maj](https://github.com/adam-maj)! - Make solc default backup compilation
- [#432](https://github.com/thirdweb-dev/js/pull/432) [`3e87dd3`](https://github.com/thirdweb-dev/js/commit/3e87dd3ccb04a2b73437357c939a251e83b783b2) Thanks [@adam-maj](https://github.com/adam-maj)! - Display file name on duplicate contract names
- [#431](https://github.com/thirdweb-dev/js/pull/431) [`c42545a`](https://github.com/thirdweb-dev/js/commit/c42545a64725492136025fa1dcdb4d46ba674a30) Thanks [@adam-maj](https://github.com/adam-maj)! - Add changeset
- [#428](https://github.com/thirdweb-dev/js/pull/428) [`6e20d1f`](https://github.com/thirdweb-dev/js/commit/6e20d1f21a622f102e94b956f700d0871c0dc779) Thanks [@adam-maj](https://github.com/adam-maj)! - Update CLI to enable create contract inside contracts project
- Updated dependencies [[`f451da6`](https://github.com/thirdweb-dev/js/commit/f451da6395689a5f89800ee63f34b6175b61f703), [`af3acc6`](https://github.com/thirdweb-dev/js/commit/af3acc6b10751b840e56aef6400da5eea6040df2), [`1bfb91d`](https://github.com/thirdweb-dev/js/commit/1bfb91dbf3a39160c987a425813ac5dbb84703ad), [`ee47840`](https://github.com/thirdweb-dev/js/commit/ee478407673b0416e0c1cfe2be11bd6963395348), [`def4251`](https://github.com/thirdweb-dev/js/commit/def42511ff5a20d83f9094164dafb87e412571b5), [`034a257`](https://github.com/thirdweb-dev/js/commit/034a257442314c67729f7bafcbe3740cff33fa32), [`eca4776`](https://github.com/thirdweb-dev/js/commit/eca47763cd89cc3b7aa57b542971837987540b55), [`639e535`](https://github.com/thirdweb-dev/js/commit/639e535ed55280ad9d081001aab3f5af72bb3e45)]:
- @thirdweb-dev/sdk@3.6.4
## 0.9.1
### Patch Changes
- [#415](https://github.com/thirdweb-dev/js/pull/415) [`48d91e4`](https://github.com/thirdweb-dev/js/commit/48d91e4431a3740d0f98f730ed5abf5b287f92c1) Thanks [@nachoiacovino](https://github.com/nachoiacovino)! - Fix mappings for prebuilts redirect on deploy
## 0.9.0
### Minor Changes
+2 -1
View File
@@ -1,6 +1,7 @@
export const CREATE_MESSAGES = {
typeOfProject: "What type of project do you want to create?",
projectName: "What is your project named?",
contractName: "What will be the name of your new smart contract?",
chain: "Which blockchain do you want to use?",
framework: "What framework do you want to use?",
language: "What language do you want to use?",
@@ -10,5 +11,5 @@ export const CREATE_MESSAGES = {
export const ERROR_MESSAGES = {
noConfiguration:
"Failed to find a supported project configuration file in current directory",
"No contract project configuration file found in current directory",
};
+4
View File
@@ -19,6 +19,10 @@ describe("npx thirdweb detect", () => {
await create.waitForText(CREATE_MESSAGES.framework);
// select hardhat
await create.pressKey("enter");
await create.waitForText(CREATE_MESSAGES.contractName);
// set contract name
await create.writeText("Contract");
await create.pressKey("enter");
await create.waitForText(CREATE_MESSAGES.contract);
// select ERC721
await create.pressKey("arrowDown");
+3 -2
View File
@@ -1,7 +1,7 @@
{
"name": "thirdweb",
"main": "dist/cli/index.js",
"version": "0.9.0",
"version": "0.9.6",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/cli",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -32,6 +32,7 @@
"@thirdweb-dev/sdk": "*",
"async-retry": "^1.3.3",
"commander": "^9.1.0",
"detect-package-manager": "^2.0.1",
"enquirer": "^2.3.6",
"ethers": "^5.7.2",
"got": "11.8.5",
@@ -73,4 +74,4 @@
"sourcemap": true,
"clean": true
}
}
}
+168 -20
View File
@@ -2,37 +2,37 @@
import { detectExtensions } from "../common/feature-detector";
import { processProject } from "../common/processor";
import { cliVersion, pkg } from "../constants/urls";
import { info, logger } from "../core/helpers/logger";
import { info, logger, spinner } from "../core/helpers/logger";
import { twCreate } from "../create/command";
import { deploy } from "../deploy";
import { findPackageInstallation } from "../helpers/detect-local-packages";
import { upload } from "../storage/command";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import chalk from "chalk";
import { exec, spawn } from "child_process";
import { Command } from "commander";
import open from "open";
import prompts from "prompts";
const main = async () => {
// eslint-disable-next-line turbo/no-undeclared-env-vars
const skipIntro = process.env.THIRDWEB_CLI_SKIP_INTRO === "true";
const program = new Command();
//yes this has to look like this, eliminates whitespace
console.info(`
$$\\ $$\\ $$\\ $$\\ $$\\
$$ | $$ | \\__| $$ | $$ |
$$$$$$\\ $$$$$$$\\ $$\\ $$$$$$\\ $$$$$$$ |$$\\ $$\\ $$\\ $$$$$$\\ $$$$$$$\\
\\_$$ _| $$ __$$\\ $$ |$$ __$$\\ $$ __$$ |$$ | $$ | $$ |$$ __$$\\ $$ __$$\\
$$ | $$ | $$ |$$ |$$ | \\__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ |
$$ |$$\\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ |
\\$$$$ |$$ | $$ |$$ |$$ | \\$$$$$$$ |\\$$$$$\\$$$$ |\\$$$$$$$\\ $$$$$$$ |
\\____/ \\__| \\__|\\__|\\__| \\_______| \\_____\\____/ \\_______|\\_______/ `);
console.info(`\n 💎 thirdweb-cli v${cliVersion} 💎\n`);
import("update-notifier").then(({ default: updateNotifier }) => {
updateNotifier({
pkg,
shouldNotifyInNpmScript: true,
// check every time while we're still building the CLI
updateCheckInterval: 0,
}).notify();
});
// yes this has to look like this, eliminates whitespace
if (!skipIntro) {
console.info(`
$$\\ $$\\ $$\\ $$\\ $$\\
$$ | $$ | \\__| $$ | $$ |
$$$$$$\\ $$$$$$$\\ $$\\ $$$$$$\\ $$$$$$$ |$$\\ $$\\ $$\\ $$$$$$\\ $$$$$$$\\
\\_$$ _| $$ __$$\\ $$ |$$ __$$\\ $$ __$$ |$$ | $$ | $$ |$$ __$$\\ $$ __$$\\
$$ | $$ | $$ |$$ |$$ | \\__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ |
$$ |$$\\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ |
\\$$$$ |$$ | $$ |$$ |$$ | \\$$$$$$$ |\\$$$$$\\$$$$ |\\$$$$$$$\\ $$$$$$$ |
\\____/ \\__| \\__|\\__|\\__| \\_______| \\_____\\____/ \\_______|\\_______/ `);
console.info(`\n 💎 thirdweb-cli v${cliVersion} 💎\n`);
}
program
.name("thirdweb-cli")
@@ -68,6 +68,10 @@ $$$$$$\\ $$$$$$$\\ $$\\ $$$$$$\\ $$$$$$$ |$$\\ $$\\ $$\\ $$$$$$\\ $$$$
"-t, --template [name]",
"A template to start your project from. You can use an template repository name from the official thirdweb-example org.",
)
.option(
"-c, --contract-name [name]",
"Name of the new smart contract to create",
)
.action(async (type, path, options) => {
await twCreate(type, path, options);
});
@@ -97,6 +101,14 @@ $$$$$$\\ $$$$$$$\\ $$\\ $$$$$$\\ $$$$$$$ |$$\\ $$\\ $$\\ $$$$$$\\ $$$$
"-n, --name [name]",
"Name of the pre-built or released contract (such as nft-drop)",
)
.option(
"-f, --file [name]",
"Filter for contract files that contain this file name",
)
.option(
"-cn, --contract-name [name]",
"Filter for contracts that contain this contract name",
)
.option(
"-cv, --contract-version [version]",
"Version of the released contract",
@@ -116,6 +128,14 @@ $$$$$$\\ $$$$$$$\\ $$\\ $$$$$$\\ $$$$$$$ |$$\\ $$\\ $$\\ $$$$$$\\ $$$$
"Release your protocol so other devs can deploy them and unlock SDKs, Dashboards and Analytics",
)
.option("-p, --path <project-path>", "path to project", ".")
.option(
"-f, --file [name]",
"Filter for contract files that contain the file name",
)
.option(
"-cn, --contract-name [name]",
"Filter for contracts that contain this contract name",
)
.option("--dry-run", "dry run (skip actually publishing)")
.option("-d, --debug", "show debug logs")
.option("--ci", "Continuous Integration mode")
@@ -178,6 +198,134 @@ $$$$$$\\ $$$$$$$\\ $$\\ $$$$$$\\ $$$$$$$ |$$\\ $$\\ $$\\ $$$$$$\\ $$$$
await detectExtensions(options);
});
if (!skipIntro) {
const versionSpinner = spinner("Checking for updates...");
await import("update-notifier").then(
async ({ default: updateNotifier }) => {
const notifier = updateNotifier({
pkg,
shouldNotifyInNpmScript: true,
// check every time while we're still building the CLI
updateCheckInterval: 0,
});
const versionInfo = await notifier.fetchInfo();
versionSpinner.stop();
if (versionInfo.type !== "latest") {
const res = await prompts({
type: "toggle",
name: "upgrade",
message: `A new version of the CLI is available. Would you like to upgrade?`,
initial: true,
active: "yes",
inactive: "no",
});
if (res.upgrade) {
const updateSpinner = spinner(
`Upgrading CLI to version ${versionInfo.latest}...`,
);
const clonedEnvironment = { ...process.env };
clonedEnvironment.THIRDWEB_CLI_SKIP_INTRO = "true";
const installation = await findPackageInstallation();
// If the package isn't installed anywhere, just defer to npx thirdweb@latest
if (!installation) {
updateSpinner.succeed(
`Now using CLI version ${versionInfo.latest}. Continuing execution...`,
);
await new Promise((done, failed) => {
const shell = spawn(
`npx --yes thirdweb@latest ${process.argv
.slice(2)
.join(" ")}`,
[],
{ stdio: "inherit", shell: true, env: clonedEnvironment },
);
shell.on("close", (code) => {
if (code === 0) {
done("");
} else {
failed();
}
});
});
return process.exit(0);
}
// Otherwise, get the correct command based on package manager and local vs. global
let command = "";
switch (installation.packageManager) {
case "npm":
command = installation.isGlobal
? `npm install -g thirdweb`
: `npm install thirdweb`;
break;
case "yarn":
command = installation.isGlobal
? `yarn global add thirdweb`
: `yarn add thirdweb`;
break;
case "pnpm":
command = installation.isGlobal
? `pnpm add -g thirdweb@latest`
: `pnpm add thirdweb@latest`;
break;
default:
console.error(
`Could not detect package manager in use, aborting automatic upgrade.\nIf you want to upgrade the CLI, please do it manually with your package manager.`,
);
process.exit(1);
}
await new Promise((done, failed) => {
exec(command, (err, stdout, stderr) => {
if (err) {
failed(err);
return;
}
done({ stdout, stderr });
});
});
updateSpinner.succeed(
`Successfully upgraded CLI to version ${versionInfo.latest}. Continuing execution...`,
);
// If the package is installed globally with yarn or pnpm, then npx won't recognize it
// So we need to make sure to run the command directly
const executionCommand =
!installation.isGlobal || installation.packageManager === "npm"
? `npx thirdweb`
: `thirdweb`;
await new Promise((done, failed) => {
const shell = spawn(
`${executionCommand} ${process.argv.slice(2).join(" ")}`,
[],
{ stdio: "inherit", shell: true, env: clonedEnvironment },
);
shell.on("close", (code) => {
if (code === 0) {
done("");
} else {
failed();
}
});
});
process.exit(0);
}
}
},
);
}
await program.parseAsync();
};
+43 -6
View File
@@ -76,7 +76,7 @@ export async function processProject(
}
}
let compiledResult;
let compiledResult: { contracts: ContractPayload[] };
const compileLoader = spinner("Compiling project...");
try {
compiledResult = await build(projectPath, projectType);
@@ -106,10 +106,47 @@ export async function processProject(
if (options.ci) {
selectedContracts = compiledResult.contracts;
} else {
const choices = compiledResult.contracts.map((c) => ({
name: c.name,
value: c,
}));
const filtered = compiledResult.contracts
.filter((c) => {
if (typeof options.file === "string" && options.file.length > 0) {
return c.fileName.includes(options.file);
}
return true;
})
.filter((c) => {
if (
typeof options.contractName === "string" &&
options.contractName.length > 0
) {
return c.name.includes(options.contractName);
}
return true;
});
if (filtered.length === 0) {
logger.error(`No contracts found matching the specified filters.`);
process.exit(1);
}
const choices = filtered.map((c) => {
if (
compiledResult.contracts.filter(
(other: ContractPayload) => other.name === c.name,
).length > 1
) {
return {
name: `${c.name} - ${chalk.gray(c.fileName)}`,
value: c,
};
}
return {
name: c.name,
value: c,
};
});
const prompt = createContractsPrompt(
choices,
`Choose which contract(s) to ${command}`,
@@ -164,7 +201,7 @@ export async function processProject(
const metadataURIs = await Promise.all(
selectedContracts.map(async (c) => {
logger.debug(`Uploading ${c.name}...`);
return await storage.upload(JSON.parse(c.metadata), {
return await storage.upload(JSON.parse(JSON.stringify(c.metadata)), {
uploadWithoutDirectory: true,
});
}),
+1
View File
@@ -55,6 +55,7 @@ export class BrownieBuilder extends BaseBuilder {
metadata: {},
bytecode,
sources: [], // TODO
fileName: "", // TODO
name: contractName,
});
break;
+20
View File
@@ -1,9 +1,11 @@
import { findFiles } from "../../common/file-helper";
import { hasForge } from "../../create/helpers/has-forge";
import { execute } from "../helpers/exec";
import { logger } from "../helpers/logger";
import { CompileOptions } from "../interfaces/Builder";
import { ContractPayload } from "../interfaces/ContractPayload";
import { BaseBuilder } from "./builder-base";
import chalk from "chalk";
import { existsSync, readFileSync } from "fs";
import { basename, join } from "path";
@@ -11,6 +13,18 @@ export class FoundryBuilder extends BaseBuilder {
public async compile(options: CompileOptions): Promise<{
contracts: ContractPayload[];
}> {
const isInstalled = await hasForge();
if (!isInstalled) {
console.error(
`\n${chalk.redBright(
`error`,
)} You don't have forge installed on this machine!\n\nYou can install forge by following these instructions:\n${chalk.blueBright(
`https://book.getfoundry.sh/getting-started/installation`,
)}\n`,
);
process.exit(1);
}
await execute("forge clean", options.projectPath);
await execute("forge build --extra-output metadata", options.projectPath);
@@ -88,6 +102,11 @@ export class FoundryBuilder extends BaseBuilder {
})
.filter((path) => path !== undefined) as string[];
const fileNames = Object.keys(
parsedMetadata?.settings?.compilationTarget || {},
);
const fileName = fileNames.length > 0 ? fileNames[0] : "";
if (
this.shouldProcessContract(
contractInfo.abi,
@@ -99,6 +118,7 @@ export class FoundryBuilder extends BaseBuilder {
name: contractName,
metadata,
bytecode,
fileName,
sources,
});
}
+7 -1
View File
@@ -25,7 +25,7 @@ export class HardhatBuilder extends BaseBuilder {
//the hardhat extractor **logs out** the runtime config of hardhat, we take that stdout and parse it
const stringifiedConfig = (
await execute(
`npx hardhat run ${configExtractorScriptPath} --no-compile`,
`npx hardhat run "${configExtractorScriptPath}" --no-compile`,
options.projectPath,
)
).stdout;
@@ -123,11 +123,17 @@ export class HardhatBuilder extends BaseBuilder {
})
.filter((path) => path !== undefined) as string[];
const fileNames = Object.keys(
meta?.settings?.compilationTarget || {},
);
const fileName = fileNames.length > 0 ? fileNames[0] : "";
if (this.shouldProcessContract(abi, deployedBytecode, contractName)) {
contracts.push({
metadata,
bytecode,
name: contractName,
fileName,
sources,
});
}
+6
View File
@@ -140,11 +140,17 @@ export class SolcBuilder extends BaseBuilder {
})
.filter((path) => path !== undefined) as string[];
const fileNames = Object.keys(
parsedMetadata?.settings?.compilationTarget || {},
);
const fileName = fileNames.length > 0 ? fileNames[0] : "";
if (this.shouldProcessContract(abi, deployedBytecode, contractName)) {
contracts.push({
metadata,
bytecode,
name: contractName,
fileName,
sources: _sources,
});
}
+7
View File
@@ -72,11 +72,18 @@ export class TruffleBuilder extends BaseBuilder {
return undefined;
})
.filter((path) => path !== undefined) as string[];
const fileNames = Object.keys(
parsedMetadata?.settings?.compilationTarget || {},
);
const fileName = fileNames.length > 0 ? fileNames[0] : "";
if (this.shouldProcessContract(abi, deployedBytecode, contractName)) {
contracts.push({
metadata,
bytecode,
name: contractName,
fileName,
sources,
});
}
@@ -36,6 +36,7 @@ export default async function detect(
name: "continue",
message:
"Do you want to continue and compile this project with solc instead?",
initial: true,
});
const shouldCompile = await prompt.run();
if (!shouldCompile) {
@@ -17,6 +17,11 @@ export interface ContractPayload {
*/
metadata: any;
/**
* The file name of the original compiled file
*/
fileName: string;
/**
* The source file paths
*/
+217 -128
View File
@@ -2,14 +2,17 @@
/* eslint-disable import/no-extraneous-dependencies */
import { CREATE_MESSAGES } from "../../constants/constants";
import detect from "../core/detection/detect";
import { DownloadError, createApp } from "./helpers/create-app";
import { createContract } from "./helpers/create-contract";
import { createContractProject } from "./helpers/create-contract";
import { getPkgManager } from "./helpers/get-pkg-manager";
import { validateNpmName } from "./helpers/validate-pkg";
import chalk from "chalk";
import fs from "fs";
import path from "path";
import prompts from "prompts";
let contractName: string = "";
let projectPath: string = "";
let projectType: string = "";
let framework: string = "";
@@ -27,6 +30,10 @@ export async function twCreate(
projectPath = pPath;
}
if (typeof options.contractName === "string") {
contractName = options.contractName;
}
if (pType === "app" || options.app) {
projectType = "app";
} else if (pType === "contract" || options.contract) {
@@ -97,123 +104,210 @@ export async function twCreate(
projectType = "app";
}
if (!projectPath) {
const defaultName =
projectType === "contract" ? "thirdweb-contracts" : "thirdweb-app";
const res = await prompts({
type: "text",
name: "path",
message: CREATE_MESSAGES.projectName,
initial: options.template || defaultName,
format: (name: string) => name.toLowerCase(),
validate: (name: string) => {
const validation = validateNpmName(
path.basename(path.resolve(name.toLowerCase())),
);
if (validation.valid) {
return true;
}
return "Invalid project name: " + validation.problems?.[0];
},
});
if (typeof res.path === "string") {
projectPath = res.path.trim();
}
}
if (!projectPath) {
console.log(
"\nPlease specify the project directory:\n" +
` ${chalk.cyan("npx thirdweb create")} ${chalk.green(
"<project-directory>",
)}\n` +
"For example:\n" +
` ${chalk.cyan("npx thirdweb create")} ${chalk.green(
"my-thirdweb-app",
)}\n\n` +
`Run ${chalk.cyan("npx thirdweb --help")} to see all options.`,
);
process.exit(1);
}
if (!options.template) {
if (projectType === "app" && !chain) {
const res = await prompts({
type: "select",
name: "chain",
message: CREATE_MESSAGES.chain,
choices: [
{ title: "EVM", value: "evm" },
{ title: "Solana", value: "solana" },
],
});
if (res.chain === "solana") {
chain = "solana";
} else {
chain = "evm";
}
}
if (projectType === "app" && !framework) {
const res = await prompts({
type: "select",
name: "framework",
message: CREATE_MESSAGES.framework,
choices:
// Solana doesn't support Vite just yet:
chain === "solana"
? [
{ title: "Next.js", value: "next" },
{ title: "Create React App", value: "cra" },
// { title: "Vite", value: "vite" },
]
: [
{ title: "Next.js", value: "next" },
{ title: "Create React App", value: "cra" },
{ title: "Vite", value: "vite" },
],
});
if (typeof res.framework === "string") {
framework = res.framework.trim();
}
}
if (projectType === "app" && !language) {
const res = await prompts({
type: "select",
name: "language",
message: CREATE_MESSAGES.language,
choices: [
{ title: "JavaScript", value: "javascript" },
{ title: "TypeScript", value: "typescript" },
],
});
if (typeof res.language === "string") {
language = res.language.trim();
}
}
// Whether to only create a new contract without the project
let onlyContract = false;
if (projectType === "contract") {
const resolvedProjectPath = path.resolve(projectPath);
const contractProjectType = await detect(resolvedProjectPath, {});
if (
projectType === "contract" &&
framework !== "forge" &&
framework !== "hardhat"
contractProjectType === "foundry" &&
fs.existsSync(path.join(resolvedProjectPath, "src"))
) {
onlyContract = true;
projectPath = path.join(projectPath, "src");
} else if (contractProjectType === "hardhat") {
if (fs.existsSync(path.join(resolvedProjectPath, "contracts"))) {
onlyContract = true;
projectPath = path.join(projectPath, "contracts");
} else {
// If there's no expected contracts folder, check for a directory that has .sol files
const directories = fs
.readdirSync(resolvedProjectPath)
.filter((file) =>
fs.lstatSync(path.join(resolvedProjectPath, file)).isDirectory(),
);
for (const directory of directories) {
const files = fs.readdirSync(path.join(projectPath, directory));
if (files.some((file) => file.endsWith(".sol"))) {
onlyContract = true;
projectPath = path.join(projectPath, directory);
break;
}
}
}
}
}
if (!onlyContract) {
if (!projectPath) {
const defaultName =
projectType === "contract" ? "thirdweb-contracts" : "thirdweb-app";
const res = await prompts({
type: "select",
name: "framework",
message: CREATE_MESSAGES.framework,
choices: [
{ title: "Hardhat", value: "hardhat" },
{ title: "Forge", value: "forge" },
],
type: "text",
name: "path",
message: CREATE_MESSAGES.projectName,
initial: options.template || defaultName,
format: (name: string) => name.toLowerCase(),
validate: (name: string) => {
const validation = validateNpmName(
path.basename(path.resolve(name.toLowerCase())),
);
if (validation.valid) {
return true;
}
return "Invalid project name: " + validation.problems?.[0];
},
});
if (typeof res.framework === "string") {
framework = res.framework.trim();
if (typeof res.path === "string") {
projectPath = path.join(projectPath, res.path.trim());
}
}
if (!projectPath) {
console.log(
"\nPlease specify the project directory:\n" +
` ${chalk.cyan("npx thirdweb create")} ${chalk.green(
"<project-directory>",
)}\n` +
"For example:\n" +
` ${chalk.cyan("npx thirdweb create")} ${chalk.green(
"my-thirdweb-app",
)}\n\n` +
`Run ${chalk.cyan("npx thirdweb --help")} to see all options.`,
);
process.exit(1);
}
if (!options.template) {
if (projectType === "app" && !chain) {
const res = await prompts({
type: "select",
name: "chain",
message: CREATE_MESSAGES.chain,
choices: [
{ title: "EVM", value: "evm" },
{ title: "Solana", value: "solana" },
],
});
if (res.chain === "solana") {
chain = "solana";
} else {
chain = "evm";
}
}
if (projectType === "app" && !framework) {
const res = await prompts({
type: "select",
name: "framework",
message: CREATE_MESSAGES.framework,
choices:
// Solana doesn't support Vite just yet:
chain === "solana"
? [
{ title: "Next.js", value: "next" },
{ title: "Create React App", value: "cra" },
// { title: "Vite", value: "vite" },
]
: [
{ title: "Next.js", value: "next" },
{ title: "Create React App", value: "cra" },
{ title: "Vite", value: "vite" },
],
});
if (typeof res.framework === "string") {
framework = res.framework.trim();
}
}
if (projectType === "app" && !language) {
const res = await prompts({
type: "select",
name: "language",
message: CREATE_MESSAGES.language,
choices: [
{ title: "JavaScript", value: "javascript" },
{ title: "TypeScript", value: "typescript" },
],
});
if (typeof res.language === "string") {
language = res.language.trim();
}
}
if (
projectType === "contract" &&
framework !== "forge" &&
framework !== "hardhat"
) {
const res = await prompts({
type: "select",
name: "framework",
message: CREATE_MESSAGES.framework,
choices: [
{ title: "Hardhat", value: "hardhat" },
{ title: "Forge", value: "forge" },
],
});
if (typeof res.framework === "string") {
framework = res.framework.trim();
}
}
if (!framework) {
console.log("Please specify a framework");
process.exit(1);
}
if (!language) {
// Default = JavaScript
language = "javascript";
}
}
}
if (projectType === "contract") {
// Select contract name
if (!contractName) {
const defaultName = "MyContract";
const res = await prompts({
type: "text",
name: "path",
message: CREATE_MESSAGES.contractName,
initial: defaultName,
validate: (name: string) => {
const isValid = /(^[a-z0-9A-Z]+$)|(^[a-z0-9A-Z]+\.sol$)/.test(name);
if (isValid) {
return true;
}
return `Invalid contract name 'contractName' (only alphanumeric characters allowed)`;
},
});
if (typeof res.path === "string") {
contractName = res.path.trim();
}
if (!contractName) {
console.log(
"\nPlease specify the contract name:\n" +
` ${chalk.cyan(
"npx thirdweb create --contract --name",
)} ${chalk.green("<contract-name>")}\n` +
"For example:\n" +
` ${chalk.cyan(
"npx thirdweb create --contract --name",
)} ${chalk.green("MyContract")}\n\n` +
`Run ${chalk.cyan("npx thirdweb --help")} to see all options.`,
);
process.exit(1);
}
}
@@ -225,7 +319,9 @@ export async function twCreate(
name: "standard",
message: CREATE_MESSAGES.contract,
choices: [
{ title: "Empty Contract", value: "none" },
...(!onlyContract
? [{ title: "Empty Contract", value: "none" }]
: []),
{ title: "ERC721", value: "erc721" },
{ title: "ERC20", value: "erc20" },
{ title: "ERC1155", value: "erc1155" },
@@ -279,20 +375,11 @@ export async function twCreate(
}
}
}
if (!framework) {
console.log("Please specify a framework");
process.exit(1);
}
if (!language) {
// Default = JavaScript
language = "javascript";
}
}
const resolvedProjectPath = path.resolve(projectPath);
const projectName = path.basename(resolvedProjectPath);
// Resolve project path
projectPath = path.resolve(projectPath);
const projectName = path.basename(projectPath);
const { valid, problems } = validateNpmName(projectName);
if (!valid) {
@@ -324,7 +411,7 @@ export async function twCreate(
try {
if (projectType === "app") {
await createApp({
appPath: resolvedProjectPath,
appPath: projectPath,
packageManager,
framework,
language,
@@ -332,11 +419,13 @@ export async function twCreate(
chain,
});
} else {
await createContract({
contractPath: resolvedProjectPath,
await createContractProject({
contractPath: projectPath,
packageManager,
framework,
contractName,
baseContract,
onlyContract,
});
}
} catch (reason) {
@@ -2,6 +2,7 @@ import { hasBaseContract, readBaseContract } from "./base-contracts";
import { DownloadError } from "./create-app";
import { PackageManager } from "./get-pkg-manager";
import { tryGitInit } from "./git";
import { hasForge } from "./has-forge";
import { install } from "./install";
import { isFolderEmpty } from "./is-folder-empty";
import { getOnline } from "./is-online";
@@ -11,22 +12,28 @@ import { submodules } from "./submodules";
import { downloadAndExtractRepo } from "./templates";
import retry from "async-retry";
import chalk from "chalk";
import fs from "fs";
import { writeFile } from "fs/promises";
import path from "path";
interface ICreateContract {
interface ICreateContractProject {
contractPath: string;
packageManager: PackageManager;
contractName: string;
framework?: string;
baseContract?: string;
onlyContract: boolean;
}
export async function createContract({
export async function createContractProject({
contractPath,
packageManager,
framework,
contractName,
baseContract,
}: ICreateContract) {
onlyContract,
}: ICreateContractProject) {
// Check that the selected base contract is valid
if (baseContract) {
const found = hasBaseContract(baseContract);
@@ -40,8 +47,8 @@ export async function createContract({
}
}
// Check that we have write permission for the specified directory
const root = path.resolve(contractPath);
if (!(await isWriteable(path.dirname(root)))) {
console.error(
"The application path is not writable, please check folder permissions and try again.",
@@ -52,117 +59,179 @@ export async function createContract({
process.exit(1);
}
const projectName = path.basename(root);
await makeDir(root);
if (!isFolderEmpty(root, projectName)) {
// Check that the contract name is valid
if (!/(^[a-z0-9A-Z]+$)|(^[a-z0-9A-Z]+\.sol$)/.test(contractName)) {
console.error(
`Contract name ${chalk.red(
`"${contractName}"`,
)} is not valid (only alphanumeric characters are allowed).`,
);
process.exit(1);
}
// Clean the contract name
contractName = contractName.replace(/\.sol$/, "") + ".sol";
const useYarn = packageManager === "yarn";
const isOnline = !useYarn || (await getOnline());
const originalDirectory = process.cwd();
console.log(
`Creating a new thirdweb contracts project in ${chalk.green(root)}.`,
);
console.log();
process.chdir(root);
function isErrorLike(err: unknown): err is { message: string } {
return (
typeof err === "object" &&
err !== null &&
typeof (err as { message?: unknown }).message === "string"
);
}
try {
console.log(`Downloading files. This might take a moment.`);
let starter = "";
if (framework === "hardhat") {
starter = "hardhat-javascript-starter";
} else if (framework === "forge") {
starter = "forge-starter";
} else {
console.error("Please provide a valid contracts framework.");
if (onlyContract) {
// If we are already in a contracts project, just create a new contract
if (fs.existsSync(path.join(root, contractName))) {
console.error(
`A contract with the name ${chalk.red(
`"${contractName}"`,
)} already exists in specified directory.`,
);
process.exit(1);
}
await retry(
() => downloadAndExtractRepo(root, { name: starter, filePath: "" }),
{
retries: 3,
},
);
// Modify the /contracts/MyContract.sol contents to match the base contract
if (baseContract && baseContract.length > 0) {
const baseContractText = readBaseContract(baseContract);
// Set the contents of the Contract.sol file to the base contract
let contractFile = "";
if (framework === "hardhat") {
contractFile = path.join(root, "contracts", "Contract.sol");
}
if (framework === "forge") {
contractFile = path.join(root, "src", "Contract.sol");
}
contractFile = path.join(root, contractName);
// Write the base contract to the MyContract.sol file
await writeFile(contractFile, baseContractText);
console.log(
`${chalk.green("Success!")} Created ${contractName} at ${contractPath}`,
);
}
} catch (reason) {
throw new DownloadError(
isErrorLike(reason) ? reason.message : String(reason),
);
}
console.log("Installing packages. This might take a couple of minutes.");
console.log();
await install(root, null, { packageManager, isOnline });
console.log();
if (tryGitInit(root)) {
console.log("Initialized a git repository.");
console.log();
}
if (framework === "forge") {
await submodules();
}
let cdpath: string;
if (path.join(originalDirectory, projectName) === contractPath) {
cdpath = projectName;
} else {
cdpath = contractPath;
}
// Otherwise, create a new contracts project
const projectName = path.basename(root);
console.log(
`${chalk.green("Success!")} Created ${projectName} at ${contractPath}`,
);
console.log();
console.log("Inside that directory, you can run several commands:");
console.log();
console.log(chalk.cyan(` ${packageManager}${useYarn ? "" : " run"} build`));
console.log(
" Compiles your contracts and detects thirdweb extensions implemented on them.",
);
console.log();
console.log(chalk.cyan(` ${packageManager}${useYarn ? "" : " run"} deploy`));
console.log(" Deploys your contracts with the thirdweb deploy flow.");
console.log();
console.log(
chalk.cyan(` ${packageManager}${useYarn ? "" : " run"} release`),
);
console.log(" Releases your contracts with the thirdweb release flow.");
console.log();
console.log("We suggest that you begin by typing:");
console.log();
console.log(chalk.cyan(" cd"), cdpath);
console.log();
await makeDir(root);
if (!isFolderEmpty(root, projectName)) {
process.exit(1);
}
const isOnline = !useYarn || (await getOnline());
const originalDirectory = process.cwd();
console.log(
`Creating a new thirdweb contracts project in ${chalk.green(root)}.`,
);
console.log();
process.chdir(root);
function isErrorLike(err: unknown): err is { message: string } {
return (
typeof err === "object" &&
err !== null &&
typeof (err as { message?: unknown }).message === "string"
);
}
try {
console.log(`Downloading files. This might take a moment.`);
let starter = "";
if (framework === "hardhat") {
starter = "hardhat-javascript-starter";
} else if (framework === "forge") {
starter = "forge-starter";
} else {
console.error("Please provide a valid contracts framework.");
process.exit(1);
}
await retry(
() => downloadAndExtractRepo(root, { name: starter, filePath: "" }),
{
retries: 3,
},
);
// Add in a new contracts file with specific base contract
if (baseContract && baseContract.length > 0) {
const baseContractText = readBaseContract(baseContract);
// Set the filename of the new file and delete the dummy Contract.sol file
let contractFile = "";
if (framework === "hardhat") {
fs.unlinkSync(path.join(root, "contracts", "Contract.sol"));
contractFile = path.join(root, "contracts", contractName);
}
if (framework === "forge") {
fs.unlinkSync(path.join(root, "src", "Contract.sol"));
contractFile = path.join(root, "src", contractName);
}
// Write the base contract to the new file
await writeFile(contractFile, baseContractText);
}
} catch (reason) {
throw new DownloadError(
isErrorLike(reason) ? reason.message : String(reason),
);
}
console.log("Installing packages. This might take a couple of minutes.");
console.log();
await install(root, null, { packageManager, isOnline });
console.log();
if (tryGitInit(root)) {
console.log("Initialized a git repository.");
console.log();
}
if (framework === "forge") {
await submodules();
}
let cdpath: string;
if (path.join(originalDirectory, projectName) === contractPath) {
cdpath = projectName;
} else {
cdpath = contractPath;
}
console.log(
`${chalk.green("Success!")} Created ${projectName} at ${contractPath}`,
);
console.log();
console.log("Inside that directory, you can run several commands:");
console.log();
console.log(
chalk.cyan(` ${packageManager}${useYarn ? "" : " run"} build`),
);
console.log(
" Compiles your contracts and detects thirdweb extensions implemented on them.",
);
console.log();
console.log(
chalk.cyan(` ${packageManager}${useYarn ? "" : " run"} deploy`),
);
console.log(" Deploys your contracts with the thirdweb deploy flow.");
console.log();
console.log(
chalk.cyan(` ${packageManager}${useYarn ? "" : " run"} release`),
);
console.log(" Releases your contracts with the thirdweb release flow.");
console.log();
console.log("We suggest that you begin by typing:");
console.log();
console.log(chalk.cyan(" cd"), cdpath);
console.log();
if (framework === "forge") {
const isInstalled = await hasForge();
if (!isInstalled) {
console.log(
`\n${chalk.yellowBright(
`warning`,
)} You don't have forge installed on this machine, and will need it to run this project! \n\nYou can install forge by following these instructions:\n${chalk.blueBright(
`https://book.getfoundry.sh/getting-started/installation`,
)}\n`,
);
}
}
}
}
@@ -0,0 +1,14 @@
import { exec } from "child_process";
export async function hasForge() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return await new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
exec("forge --version", (error, stdout, stderr) => {
if (error) {
resolve(false);
}
resolve(true);
});
});
}
@@ -0,0 +1,103 @@
import { exec } from "child_process";
type PackageManager = "npm" | "yarn" | "pnpm";
interface Installation {
packageManager: PackageManager;
isGlobal: boolean;
}
export async function findPackageInstallation(): Promise<
Installation | undefined
> {
const isLocal = await isInstalledLocally();
if (isLocal) {
const packageManager = await import("detect-package-manager").then(
({ detect }) => {
return detect();
},
);
return { packageManager, isGlobal: false };
}
const isGlobalNpm = await isInstalledGloballyWithNpm();
if (isGlobalNpm) {
return { packageManager: "npm", isGlobal: true };
}
const isGlobalYarn = await isInstalledGloballyWithYarn();
if (isGlobalYarn) {
return { packageManager: "yarn", isGlobal: true };
}
const isGlobalPnpm = await isInstalledGloballyWithPnpm();
if (isGlobalPnpm) {
return { packageManager: "pnpm", isGlobal: true };
}
return undefined;
}
// For LOCAL packages, npm ls picks up things installed by yarn and pnpm too
async function isInstalledLocally(): Promise<boolean> {
const packages = await detectPackages("npm ls --depth=0");
return containsThirdweb(packages || []);
}
// But for GLOBAL packages, we need to use a separate command for each one
async function isInstalledGloballyWithNpm(): Promise<boolean> {
const packages = await detectPackages("npm ls -g --depth=0");
return containsThirdweb(packages || []);
}
async function isInstalledGloballyWithYarn(): Promise<boolean> {
const packages = await detectPackages("yarn --cwd `yarn global dir` list");
return containsThirdweb(packages || []);
}
async function isInstalledGloballyWithPnpm(): Promise<boolean> {
const packages: string[] = await new Promise((resolve) => {
exec(`pnpm list -g --depth=0`, (err, stdout) => {
var pkgs: string[] = [];
pkgs = stdout.split("dependencies:")[1]?.trim().split("\n") || [];
pkgs = pkgs.map((pkg) => pkg.replace(" ", "@"));
resolve(pkgs);
});
});
return containsThirdweb(packages || []);
}
function containsThirdweb(packages: string[]) {
return packages.some((pkg) => pkg.match(/^thirdweb/));
}
async function detectPackages(cmd: string): Promise<string[]> {
return new Promise((resolve) => {
exec(cmd, (err, stdout) => {
var packages: string[] = [];
packages = stdout.split("\n");
// We're one short on '-' to support both yarn and npm
packages = packages.filter(function (item) {
if (item.match(/^├─.+/g) !== null) {
return true;
}
if (item.match(/^└─.+/g) !== null) {
return true;
}
return undefined;
});
packages = packages
.map(function (item) {
if (item.match(/^├─.+/g) !== null) {
return item.replace(/(^├──\s|^├─\s)/g, "");
}
if (item.match(/^└─.+/g) !== null) {
return item.replace(/(^└──\s|^├─\s)/g, "");
}
})
.filter((item) => !!item) as string[];
resolve(packages);
});
});
}
@@ -13,18 +13,18 @@ export default function generateDashboardUrl(
// Pre-built contract
else {
const mapping = {
"signature-drop": "drop/signature-drop",
"nft-drop": "drop/nft-drop",
"edition-drop": "drop/edition-drop",
"nft-collection": "token/nft-collection",
edition: "token/edition",
"token-drop": "drop/token-drop",
token: "token/token",
marketplace: "marketplace/marketplace",
split: "governance/split",
vote: "governance/vote",
pack: "token/pack",
multiwrap: "token/multiwrap",
"signature-drop": "SignatureDrop",
"nft-drop": "DropERC721",
"edition-drop": "DropERC1155",
"nft-collection": "TokenERC721",
edition: "TokenERC1155",
"token-drop": "DropERC20",
token: "TokenERC20",
marketplace: "Marketplace",
split: "Split",
vote: "VoteERC20",
pack: "Pack",
multiwrap: "Multiwrap",
};
const foundMapping = mapping[name as keyof typeof mapping];
@@ -33,6 +33,6 @@ export default function generateDashboardUrl(
return undefined;
}
return `https://thirdweb.com/contracts/new/pre-built/${foundMapping}`;
return `https://thirdweb.com/thirdweb.eth/${foundMapping}`;
}
}
+6
View File
@@ -1,5 +1,11 @@
# @thirdweb-dev/contracts-js
## 1.2.2
### Patch Changes
- [#438](https://github.com/thirdweb-dev/js/pull/438) [`f451da6`](https://github.com/thirdweb-dev/js/commit/f451da6395689a5f89800ee63f34b6175b61f703) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix SigMint detection for OSRF NFT Collection contracts
## 1.2.1
### Patch Changes
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@thirdweb-dev/contracts-js",
"version": "1.2.1",
"version": "1.2.2",
"main": "dist/thirdweb-dev-contracts-js.cjs.js",
"module": "dist/thirdweb-dev-contracts-js.esm.js",
"license": "Apache-2.0",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/contracts-js",
"dependencies": {
"@thirdweb-dev/contracts": "^3.2.2"
"@thirdweb-dev/contracts": "3.2.8-0"
},
"devDependencies": {
"@babel/preset-env": "^7.18.10",
+6
View File
@@ -0,0 +1,6 @@
{
"presets": [
"@babel/preset-typescript",
["@babel/preset-env", { "targets": "defaults, not ie 11" }]
]
}
+4
View File
@@ -0,0 +1,4 @@
# folders
dist/
# files
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ["thirdweb"],
};
+7
View File
@@ -0,0 +1,7 @@
# @thirdweb-dev/pay
## 0.1.0
### Minor Changes
- [#433](https://github.com/thirdweb-dev/js/pull/433) [`7269268`](https://github.com/thirdweb-dev/js/commit/72692689e9adb41e92038765fd01a36c2b7457e1) Thanks [@jnsdls](https://github.com/jnsdls)! - Initialize Pay Package
+16
View File
@@ -0,0 +1,16 @@
<p align="center">
<br />
<a href="https://thirdweb.com"><img src="https://github.com/thirdweb-dev/js/blob/main/packages/sdk/logo.svg?raw=true" width="200" alt=""/></a>
<br />
</p>
<h1 align="center">thirdweb wallets</h1>
<p align="center">
<!-- <a href="https://www.npmjs.com/package/@thirdweb-dev/sdk"><img src="https://img.shields.io/npm/v/@thirdweb-dev/sdk?color=red&label=npm&logo=npm" alt="npm version"/></a> -->
<!-- <a href="https://github.com/thirdweb-dev/js/actions/workflows/CI.yml"><img alt="Build Status" src="https://github.com/thirdweb-dev/js/actions/workflows/CI.yml/badge.svg"/></a> -->
<!-- <a href="https://discord.gg/thirdweb"><img alt="Join our Discord!" src="https://img.shields.io/discord/834227967404146718.svg?color=7289da&label=discord&logo=discord&style=flat"/></a> -->
</p>
<!-- <p align="center"><strong>Best in class Web3 SDK for Browser, Node and Mobile apps</strong></p> -->
<br />
### todo
@@ -0,0 +1,4 @@
{
"main": "dist/thirdweb-dev-pay-integrations-coinbase.cjs.js",
"module": "dist/thirdweb-dev-pay-integrations-coinbase.esm.js"
}
+53
View File
@@ -0,0 +1,53 @@
{
"name": "@thirdweb-dev/pay",
"version": "0.1.0",
"main": "dist/thirdweb-dev-pay.cjs.js",
"module": "dist/thirdweb-dev-pay.esm.js",
"exports": {
".": {
"module": "./dist/thirdweb-dev-pay.esm.js",
"default": "./dist/thirdweb-dev-pay.cjs.js"
},
"./integrations/coinbase": {
"module": "./integrations/coinbase/dist/thirdweb-dev-pay-integrations-coinbase.esm.js",
"default": "./integrations/coinbase/dist/thirdweb-dev-pay-integrations-coinbase.cjs.js"
},
"./package.json": "./package.json"
},
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/pay",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/thirdweb-dev/js/issues"
},
"author": "thirdweb eng <[email protected]>",
"files": [
"dist/",
"integrations/"
],
"preconstruct": {
"entrypoints": [
"index.ts",
"integrations/**"
],
"___experimentalFlags_WILL_CHANGE_IN_PATCH": {
"exports": true
},
"exports": true
},
"sideEffects": false,
"dependencies": {
"@coinbase/cbpay-js": "^1.6.0"
},
"devDependencies": {
"@preconstruct/cli": "^2.2.1",
"typescript": "^4.7.4"
},
"scripts": {
"format": "prettier --write 'src/**/*'",
"lint": "eslint src/",
"fix": "eslint src/ --fix",
"clean": "rm -rf dist/",
"build": "tsc && preconstruct build",
"push": "yalc push"
}
}
+1
View File
@@ -0,0 +1 @@
export * from "./integrations/coinbase";
@@ -0,0 +1,62 @@
import { initOnRamp, InitOnRampParams } from "@coinbase/cbpay-js";
interface CoinbasePayOptions extends Pick<InitOnRampParams, "appId"> {}
const CHAIN_ID_MAP = {
[-1]: "solana",
1: "ethereum",
69: "optimism",
137: "polygon",
} as const;
export type FundWalletOptions = {
address: string;
chainId: keyof typeof CHAIN_ID_MAP;
assets?: string[];
};
export class CoinbasePayIntegration {
#appId: string;
constructor(options: CoinbasePayOptions) {
this.#appId = options.appId;
}
async fundWallet(opts: FundWalletOptions): Promise<void> {
const { address, chainId, assets } = opts;
return new Promise((res, rej) => {
initOnRamp(
{
appId: this.#appId,
widgetParameters: {
destinationWallets: [
{
address,
assets,
supportedNetworks: [CHAIN_ID_MAP[chainId]],
},
],
},
experienceLoggedIn: "embedded",
experienceLoggedOut: "popup",
closeOnExit: true,
onSuccess: () => {
res();
},
onExit(error) {
if (error) {
return rej(error);
}
return res();
},
},
(err, instance) => {
if (err || !instance) {
return rej(err);
}
instance.open();
},
);
});
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "@thirdweb-dev/tsconfig/sdk.json",
"include": ["src", "types"],
"exclude": ["dist", "build", "node_modules"]
}
+22
View File
@@ -1,5 +1,27 @@
# @thirdweb-dev/react
## 3.6.7
### Patch Changes
- [#448](https://github.com/thirdweb-dev/js/pull/448) [`7a37e56`](https://github.com/thirdweb-dev/js/commit/7a37e564fd5d5a9df84c8da44ecaf6c42f67a0e2) Thanks [@jnsdls](https://github.com/jnsdls)! - [EVM] - allow `useContract` to fail faster to enable the import case
## 3.6.6
### Patch Changes
- [#443](https://github.com/thirdweb-dev/js/pull/443) [`8c6cdaa`](https://github.com/thirdweb-dev/js/commit/8c6cdaa2887fb2cc40d3ee6991233d195d103805) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix magic link connector not connecting to the right chain
## 3.6.5
## 3.6.4
### Patch Changes
- [#425](https://github.com/thirdweb-dev/js/pull/425) [`f545a67`](https://github.com/thirdweb-dev/js/commit/f545a67e9fb597d27bb39ca515d24d306fbb121a) Thanks [@adam-maj](https://github.com/adam-maj)! - Add sameSite none and include credentials
- [#420](https://github.com/thirdweb-dev/js/pull/420) [`639e535`](https://github.com/thirdweb-dev/js/commit/639e535ed55280ad9d081001aab3f5af72bb3e45) Thanks [@jnsdls](https://github.com/jnsdls)! - update deps
## 3.6.3
## 3.6.2
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react",
"version": "3.6.3",
"version": "3.6.7",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -14,19 +14,19 @@ export const SupportedNetworkSelect: React.FC<SupportedNetworkSelectProps> = ({
const deprecatedNetworks = useMemo(() => {
return SUPPORTED_CHAIN_IDS.map((supportedChain) => {
return getChainFromChainId(supportedChain);
}).filter((n) => !!n.deprecated);
});
}, []);
const testnets = useMemo(() => {
return SUPPORTED_CHAIN_IDS.map((supportedChain) => {
return getChainFromChainId(supportedChain);
}).filter((n) => n.testnet && !n.deprecated);
});
}, []);
const mainnets = useMemo(() => {
return SUPPORTED_CHAIN_IDS.map((supportedChain) => {
return getChainFromChainId(supportedChain);
}).filter((n) => !n.testnet && !n.deprecated);
});
}, []);
return (
<Select {...selectProps}>
@@ -103,7 +103,10 @@ export class GnosisSafeConnector extends Connector {
import("@gnosis.pm/safe-ethers-lib"),
]);
const ethAdapter = new safeEthersLib.default({ ethers, signer });
const ethAdapter = new safeEthersLib.default({
ethers,
signerOrProvider: signer,
});
const safe = await safeCoreSdk.default.create({
ethAdapter: ethAdapter as any,
@@ -62,6 +62,18 @@ export class MagicConnector extends Connector {
const { apiKey, ...options } = this.options;
const configuration = this.getConfiguration();
const chains = Object.entries(this.options.rpcUrls);
if (chains.length > 1) {
console.warn(
`Magic only supports connecting to one chain at a time. Using the first chain specified in rpcUrls: ${chains[0]}.`,
);
}
const [chainId, rpcUrl] = chains[0];
this.options.network = {
chainId: parseInt(chainId),
rpcUrl,
};
try {
invariant(
configuration,
+1 -1
View File
@@ -2,7 +2,7 @@ import { NATIVE_TOKENS, SUPPORTED_CHAIN_ID } from "@thirdweb-dev/sdk";
import { ChainId } from "@thirdweb-dev/sdk";
import type { Chain as WagmiChain } from "wagmi";
export type Chain = WagmiChain & { deprecated?: boolean };
export type Chain = WagmiChain;
const chain: Record<string, Chain> = {
mainnet: {
id: ChainId.Mainnet,
@@ -238,7 +238,7 @@ export function useContract(
await queryClient.fetchQuery(
compilerMetadata.cacheKey(contractAddress, activeChainId),
() => compilerMetadata.fetchQuery(contractAddress, sdk),
{ cacheTime: Infinity, staleTime: Infinity },
{ cacheTime: Infinity, staleTime: Infinity, retry: 0 },
)
)?.abi;
}
@@ -258,6 +258,8 @@ export function useContract(
cacheTime: Infinity,
staleTime: Infinity,
enabled: !!contractAddress && !!sdk && !!activeChainId,
// never retry
retry: 0,
},
);
+3 -1
View File
@@ -24,7 +24,9 @@ export function useUser() {
authConfig,
"Please specify an authConfig in the ThirdwebProvider",
);
const res = await fetch(`${authConfig.authUrl}/user`);
const res = await fetch(`${authConfig.authUrl}/user`, {
credentials: "include",
});
return (await res.json()) as ThirdwebAuthUser;
},
{
@@ -24,7 +24,9 @@ export function useUser() {
authConfig,
"Please specify an authConfig in the ThirdwebProvider",
);
const res = await fetch(`${authConfig.authUrl}/user`);
const res = await fetch(`${authConfig.authUrl}/user`, {
credentials: "include",
});
return (await res.json()) as ThirdwebAuthUser;
},
{
+45
View File
@@ -1,5 +1,50 @@
# @thirdweb-dev/sdk
## 3.6.7
### Patch Changes
- [#451](https://github.com/thirdweb-dev/js/pull/451) [`3731459`](https://github.com/thirdweb-dev/js/commit/3731459d33f1ded7ebb69124809449b901b3ad3d) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Less strict prebuilt checks to support OSRF variants
- [#450](https://github.com/thirdweb-dev/js/pull/450) [`cac6c30`](https://github.com/thirdweb-dev/js/commit/cac6c30bca5e17df81d746ef81316af47d5e252e) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix setting allowance or approvals gaslessly
- [#448](https://github.com/thirdweb-dev/js/pull/448) [`7a37e56`](https://github.com/thirdweb-dev/js/commit/7a37e564fd5d5a9df84c8da44ecaf6c42f67a0e2) Thanks [@jnsdls](https://github.com/jnsdls)! - [EVM] - allow `useContract` to fail faster to enable the import case
## 3.6.6
### Patch Changes
- [#443](https://github.com/thirdweb-dev/js/pull/443) [`8c6cdaa`](https://github.com/thirdweb-dev/js/commit/8c6cdaa2887fb2cc40d3ee6991233d195d103805) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix signature minting from OpenSea Royalty Filter NFT collection contracts
## 3.6.5
### Patch Changes
- [#440](https://github.com/thirdweb-dev/js/pull/440) [`efc56fa`](https://github.com/thirdweb-dev/js/commit/efc56fa5802490ac8ef50037658d046afd89e9a1) Thanks [@jnsdls](https://github.com/jnsdls)! - [EVM] - providers are now re-used if the constructor options are identical leading to better batching, also introduced an additional max batch size param (250 by default)
## 3.6.4
### Patch Changes
- [#438](https://github.com/thirdweb-dev/js/pull/438) [`f451da6`](https://github.com/thirdweb-dev/js/commit/f451da6395689a5f89800ee63f34b6175b61f703) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix SigMint detection for OSRF NFT Collection contracts
- [#416](https://github.com/thirdweb-dev/js/pull/416) [`af3acc6`](https://github.com/thirdweb-dev/js/commit/af3acc6b10751b840e56aef6400da5eea6040df2) Thanks [@adam-maj](https://github.com/adam-maj)! - Add support for AWS KMS
- [#424](https://github.com/thirdweb-dev/js/pull/424) [`1bfb91d`](https://github.com/thirdweb-dev/js/commit/1bfb91dbf3a39160c987a425813ac5dbb84703ad) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Improve error parsing for other wallets
- [#436](https://github.com/thirdweb-dev/js/pull/436) [`ee47840`](https://github.com/thirdweb-dev/js/commit/ee478407673b0416e0c1cfe2be11bd6963395348) Thanks [@jnsdls](https://github.com/jnsdls)! - fix #429
- [#434](https://github.com/thirdweb-dev/js/pull/434) [`def4251`](https://github.com/thirdweb-dev/js/commit/def42511ff5a20d83f9094164dafb87e412571b5) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - [SOL] Remove workaround to fetch NFT owners
- [#429](https://github.com/thirdweb-dev/js/pull/429) [`034a257`](https://github.com/thirdweb-dev/js/commit/034a257442314c67729f7bafcbe3740cff33fa32) Thanks [@jakeloo](https://github.com/jakeloo)! - Extract more minimal proxy impl address
- [#418](https://github.com/thirdweb-dev/js/pull/418) [`eca4776`](https://github.com/thirdweb-dev/js/commit/eca47763cd89cc3b7aa57b542971837987540b55) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Increase gas limit for opening packs
- [#420](https://github.com/thirdweb-dev/js/pull/420) [`639e535`](https://github.com/thirdweb-dev/js/commit/639e535ed55280ad9d081001aab3f5af72bb3e45) Thanks [@jnsdls](https://github.com/jnsdls)! - update deps
- Updated dependencies [[`f451da6`](https://github.com/thirdweb-dev/js/commit/f451da6395689a5f89800ee63f34b6175b61f703)]:
- @thirdweb-dev/contracts-js@1.2.2
## 3.6.3
### Patch Changes
+9 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/sdk",
"version": "3.6.3",
"version": "3.6.7",
"description": "The main thirdweb SDK.",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/sdk",
"license": "Apache-2.0",
@@ -97,6 +97,7 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-tsdoc": "^0.2.16",
"ethers": "^5.7.2",
"ethers-aws-kms-signer": "^1.3.2",
"hardhat": "^2.9.3",
"mocha": "^10.0.0",
"prettier": "^2.7.1",
@@ -104,11 +105,15 @@
},
"peerDependencies": {
"@aws-sdk/client-secrets-manager": "^3.215.0",
"ethers": "^5"
"ethers": "^5",
"ethers-aws-kms-signer": "^1.3.2"
},
"peerDependenciesMeta": {
"@aws-sdk/client-secrets-manager": {
"optional": true
},
"ethers-aws-kms-signer": {
"optional": true
}
},
"dependencies": {
@@ -122,7 +127,7 @@
"bn.js": "^5.2.1",
"bs58": "^5.0.0",
"cross-fetch": "^3.1.5",
"eventemitter3": "^4.0.7",
"eventemitter3": "^5.0.0",
"fast-deep-equal": "^3.1.3",
"merkletreejs": "^0.2.24",
"tiny-invariant": "^1.2.0",
@@ -141,4 +146,4 @@
"url": "https://github.com/thirdweb-dev/js/issues"
},
"author": "thirdweb eng <[email protected]>"
}
}
+2 -2
View File
@@ -124,7 +124,7 @@ export async function setErc20Allowance(
signer || provider,
currencyAddress,
ERC20Abi,
{},
contractToApprove.options,
);
const owner = await contractToApprove.getSignerAddress();
@@ -151,7 +151,7 @@ export async function approveErc20Allowance(
signer || provider,
currencyAddress,
ERC20Abi,
{},
contractToApprove.options,
);
const owner = await contractToApprove.getSignerAddress();
const spender = contractToApprove.readContract.address;
+3 -1
View File
@@ -401,7 +401,9 @@ export async function convertToTWError(
// not sure what this is, just throw it back
raw = error.toString();
}
const reason = parseMessageParts(/.*?"message[^a-zA-Z0-9]*([^"\\]*).*?/, raw);
const reason =
parseMessageParts(/.*?"message[^a-zA-Z0-9]*([^"\\]*).*?/, raw) ||
parseMessageParts(/.*?"reason[^a-zA-Z0-9]*([^"\\]*).*?/, raw);
const data = parseMessageParts(/.*?"data[^a-zA-Z0-9]*([^"\\]*).*?/, raw);
const rpcUrl = parseMessageParts(/.*?"url[^a-zA-Z0-9]*([^"\\]*).*?/, raw);
let from = parseMessageParts(/.*?"from[^a-zA-Z0-9]*([^"\\]*).*?/, raw);
@@ -266,6 +266,42 @@ function toJSType(
return jsType;
}
/**
* @internal
* @param bytecode
*/
export function extractMinimalProxyImplementationAddress(
bytecode: string,
): string | undefined {
// EIP-1167 clone minimal proxy - https://eips.ethereum.org/EIPS/eip-1167
if (bytecode.startsWith("0x363d3d373d3d3d363d73")) {
const implementationAddress = bytecode.slice(22, 62);
return `0x${implementationAddress}`;
}
// Minimal Proxy with receive() from 0xSplits - https://github.com/0xSplits/splits-contracts/blob/c7b741926ec9746182d0d1e2c4c2046102e5d337/contracts/libraries/Clones.sol
if (bytecode.startsWith("0x36603057343d5230")) {
// +40 = size of addr
const implementationAddress = bytecode.slice(122, 122 + 40);
return `0x${implementationAddress}`;
}
// 0age's minimal proxy - https://medium.com/coinmonks/the-more-minimal-proxy-5756ae08ee48
if (bytecode.startsWith("0x3d3d3d3d363d3d37363d73")) {
// +40 = size of addr
const implementationAddress = bytecode.slice(24, 24 + 40);
return `0x${implementationAddress}`;
}
// vyper's minimal proxy (uniswap v1) - https://etherscan.io/address/0x09cabec1ead1c0ba254b09efb3ee13841712be14#code
if (bytecode.startsWith("0x366000600037611000600036600073")) {
const implementationAddress = bytecode.slice(32, 32 + 40);
return `0x${implementationAddress}`;
}
return undefined;
}
/**
* @internal
* @param address
@@ -282,14 +318,20 @@ export async function resolveContractUriFromAddress(
`Contract at ${address} does not exist on chain '${chain.name}' (chainId: ${chain.chainId})`,
);
}
// EIP-1167 clone proxy - https://eips.ethereum.org/EIPS/eip-1167
if (bytecode.startsWith("0x363d3d373d3d3d363d")) {
const implementationAddress = bytecode.slice(22, 62);
return await resolveContractUriFromAddress(
`0x${implementationAddress}`,
provider,
);
try {
const implementationAddress =
extractMinimalProxyImplementationAddress(bytecode);
if (implementationAddress) {
return await resolveContractUriFromAddress(
implementationAddress,
provider,
);
}
} catch (e) {
// ignore
}
// EIP-1967 proxy storage slots - https://eips.ethereum.org/EIPS/eip-1967
try {
const proxyStorage = await provider.getStorageAt(
@@ -400,6 +442,11 @@ export async function fetchContractMetadata(
storage: ThirdwebStorage,
): Promise<PublishedMetadata> {
const metadata = await storage.downloadJSON(compilerMetadataUri);
if (!metadata || !metadata.output) {
throw new Error(
`Could not resolve metadata for contract at ${compilerMetadataUri}`,
);
}
const abi = AbiSchema.parse(metadata.output.abi);
const compilationTarget = metadata.settings.compilationTarget;
const targets = Object.keys(compilationTarget);
+16 -12
View File
@@ -3,7 +3,6 @@ import {
InterfaceId_IERC1155,
InterfaceId_IERC721,
} from "../constants/contract";
import { SignerOrProvider } from "../core";
import { ContractWrapper } from "../core/classes/contract-wrapper";
import {
NewAuctionListing,
@@ -76,33 +75,38 @@ export async function isTokenApprovedForTransfer(
/**
* Checks if the marketplace is approved to make transfers on the assetContract
* If not, it tries to set the approval.
* @param signerOrProvider
* @param contractWrapper
* @param marketplaceAddress
* @param assetContract
* @param tokenId
* @param from
*/
export async function handleTokenApproval(
signerOrProvider: SignerOrProvider,
contractWrapper: ContractWrapper<any>,
marketplaceAddress: string,
assetContract: string,
tokenId: BigNumberish,
from: string,
): Promise<void> {
const erc165 = new Contract(
const erc165 = new ContractWrapper<IERC165>(
contractWrapper.getSignerOrProvider(),
assetContract,
ERC165Abi,
signerOrProvider,
) as IERC165;
const isERC721 = await erc165.supportsInterface(InterfaceId_IERC721);
const isERC1155 = await erc165.supportsInterface(InterfaceId_IERC1155);
contractWrapper.options,
);
const isERC721 = await erc165.readContract.supportsInterface(
InterfaceId_IERC721,
);
const isERC1155 = await erc165.readContract.supportsInterface(
InterfaceId_IERC1155,
);
// check for token approval
if (isERC721) {
const asset = new ContractWrapper<IERC721>(
signerOrProvider,
contractWrapper.getSignerOrProvider(),
assetContract,
ERC721Abi,
{},
contractWrapper.options,
);
const approved = await asset.readContract.isApprovedForAll(
from,
@@ -122,10 +126,10 @@ export async function handleTokenApproval(
}
} else if (isERC1155) {
const asset = new ContractWrapper<IERC1155>(
signerOrProvider,
contractWrapper.getSignerOrProvider(),
assetContract,
ERC1155Abi,
{},
contractWrapper.options,
);
const approved = await asset.readContract.isApprovedForAll(
@@ -12,8 +12,8 @@ import ILazyMintAbi from "@thirdweb-dev/contracts-js/dist/abis/ILazyMint.json";
import IMintableERC721Abi from "@thirdweb-dev/contracts-js/dist/abis/IMintableERC721.json";
import MulticallAbi from "@thirdweb-dev/contracts-js/dist/abis/IMulticall.json";
import SignatureMintERC721Abi from "@thirdweb-dev/contracts-js/dist/abis/ISignatureMintERC721.json";
import SignatureMintERC721_V1Abi from "@thirdweb-dev/contracts-js/dist/abis/ISignatureMintERC721_V1.json";
import TieredDropAbi from "@thirdweb-dev/contracts-js/dist/abis/LazyMintWithTier.json";
import TokenERC721Abi from "@thirdweb-dev/contracts-js/dist/abis/TokenERC721.json";
export const FEATURE_NFT_BURNABLE = {
name: "ERC721Burnable",
@@ -163,7 +163,7 @@ export const FEATURE_NFT_SIGNATURE_MINTABLE_V1 = {
sdk: "sdk.erc721signaturemint",
contracts: "ISignatureMintERC721",
},
abis: [TokenERC721Abi],
abis: [SignatureMintERC721_V1Abi],
features: {},
} as const;
+14 -1
View File
@@ -123,6 +123,11 @@ export function getProviderForNetwork(network: ChainOrRpc | SignerOrProvider) {
}
}
const READONLY_PROVIDER_MAP: Map<
string,
StaticJsonRpcBatchProvider | providers.JsonRpcBatchProvider
> = new Map();
/**
*
* @param network - the chain name or rpc url
@@ -136,11 +141,19 @@ export function getReadOnlyProvider(network: string, chainId?: number) {
if (match) {
switch (match[1]) {
case "http":
return chainId
const seralizedOpts = `${network}-${chainId || -1}`;
const existingProvider = READONLY_PROVIDER_MAP.get(seralizedOpts);
if (existingProvider) {
return existingProvider;
}
const newProvider = chainId
? // if we know the chainId we should use the StaticJsonRpcBatchProvider
new StaticJsonRpcBatchProvider(network, chainId)
: // otherwise fall back to the built in json rpc batch provider
new providers.JsonRpcBatchProvider(network, chainId);
READONLY_PROVIDER_MAP.set(seralizedOpts, newProvider);
return newProvider;
case "ws":
return new providers.WebSocketProvider(network, chainId);
+14 -38
View File
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { getPrebuiltInfo } from "../common/legacy";
import { ALL_ROLES } from "../common/role";
import { getSignerAndProvider } from "../core/classes/rpc-connection-handler";
@@ -56,12 +57,8 @@ export const EditionDropInitializer = {
);
},
getAbi: async (address: string, provider: ethers.providers.Provider) => {
const contractInfo = await assertContractType(
address,
provider,
"edition-drop",
);
return contractInfo.version > 2
const contractInfo = await getContractInfo(address, provider);
return !contractInfo || contractInfo.version > 2
? (await import("@thirdweb-dev/contracts-js/dist/abis/DropERC1155.json"))
.default
: (
@@ -97,7 +94,6 @@ export const EditionInitializer = {
);
},
getAbi: async (address: string, provider: ethers.providers.Provider) => {
await assertContractType(address, provider, "edition");
return (
await import("@thirdweb-dev/contracts-js/dist/abis/TokenERC1155.json")
).default;
@@ -129,7 +125,6 @@ export const MarketplaceInitializer = {
);
},
getAbi: async (address: string, provider: ethers.providers.Provider) => {
await assertContractType(address, provider, "marketplace");
return (
await import("@thirdweb-dev/contracts-js/dist/abis/Marketplace.json")
).default;
@@ -161,7 +156,6 @@ export const MultiwrapInitializer = {
);
},
getAbi: async (address: string, provider: ethers.providers.Provider) => {
await assertContractType(address, provider, "multiwrap");
return (await import("@thirdweb-dev/contracts-js/dist/abis/Multiwrap.json"))
.default;
},
@@ -193,7 +187,6 @@ export const NFTCollectionInitializer = {
);
},
getAbi: async (address: string, provider: ethers.providers.Provider) => {
await assertContractType(address, provider, "nft-collection");
return (
await import("@thirdweb-dev/contracts-js/dist/abis/TokenERC721.json")
).default;
@@ -225,12 +218,8 @@ export const NFTDropInitializer = {
);
},
getAbi: async (address: string, provider: ethers.providers.Provider) => {
const contractInfo = await assertContractType(
address,
provider,
"nft-drop",
);
return contractInfo.version > 3
const contractInfo = await getContractInfo(address, provider);
return !contractInfo || contractInfo.version > 3
? (await import("@thirdweb-dev/contracts-js/dist/abis/DropERC721.json"))
.default
: (
@@ -267,7 +256,6 @@ export const PackInitializer = {
);
},
getAbi: async (address: string, provider: ethers.providers.Provider) => {
await assertContractType(address, provider, "pack");
return (await import("@thirdweb-dev/contracts-js/dist/abis/Pack.json"))
.default;
},
@@ -299,12 +287,8 @@ export const SignatureDropInitializer = {
);
},
getAbi: async (address: string, provider: ethers.providers.Provider) => {
const contractInfo = await assertContractType(
address,
provider,
"signature-drop",
);
return contractInfo.version > 4
const contractInfo = await getContractInfo(address, provider);
return !contractInfo || contractInfo.version > 4
? (
await import(
"@thirdweb-dev/contracts-js/dist/abis/SignatureDrop.json"
@@ -344,7 +328,6 @@ export const SplitInitializer = {
);
},
getAbi: async (address: string, provider: ethers.providers.Provider) => {
await assertContractType(address, provider, "split");
return (await import("@thirdweb-dev/contracts-js/dist/abis/Split.json"))
.default;
},
@@ -376,12 +359,8 @@ export const TokenDropInitializer = {
);
},
getAbi: async (address: string, provider: ethers.providers.Provider) => {
const contractInfo = await assertContractType(
address,
provider,
"token-drop",
);
return contractInfo.version > 2
const contractInfo = await getContractInfo(address, provider);
return !contractInfo || contractInfo.version > 2
? (await import("@thirdweb-dev/contracts-js/dist/abis/DropERC20.json"))
.default
: (await import("@thirdweb-dev/contracts-js/dist/abis/DropERC20_V2.json"))
@@ -414,7 +393,6 @@ export const TokenInitializer = {
);
},
getAbi: async (address: string, provider: ethers.providers.Provider) => {
await assertContractType(address, provider, "token");
return (
await import("@thirdweb-dev/contracts-js/dist/abis/TokenERC20.json")
).default;
@@ -447,22 +425,20 @@ export const VoteInitializer = {
);
},
getAbi: async (address: string, provider: ethers.providers.Provider) => {
await assertContractType(address, provider, "vote");
return (await import("@thirdweb-dev/contracts-js/dist/abis/VoteERC20.json"))
.default;
},
};
async function assertContractType(
async function getContractInfo(
address: string,
provider: ethers.providers.Provider,
type: PrebuiltContractType,
) {
const contractInfo = await getPrebuiltInfo(address, provider);
if (!contractInfo || contractInfo.type !== getContractName(type)) {
throw new Error(`Contract is not a ${type}`);
try {
return await getPrebuiltInfo(address, provider);
} catch (e) {
return undefined;
}
return contractInfo;
}
/**
@@ -536,10 +536,14 @@ export class Pack extends StandardErc1155<PackContract> {
tokenId: BigNumberish,
amount: BigNumberish = 1,
): Promise<PackRewards> {
const receipt = await this.contractWrapper.sendTransaction("openPack", [
tokenId,
amount,
]);
const receipt = await this.contractWrapper.sendTransaction(
"openPack",
[tokenId, amount],
{
// Higher gas limit for opening packs
gasLimit: 500000,
},
);
const event = this.contractWrapper.parseLogs<PackOpenedEvent>(
"PackOpened",
receipt?.logs,
@@ -3,7 +3,7 @@ import { ContractEvent, EventQueryOptions } from "../../types/index";
import { ContractWrapper } from "./contract-wrapper";
import { EventFragment } from "@ethersproject/abi";
import { BaseContract, Event, providers } from "ethers";
import { ListenerFn } from "eventemitter3";
import type { EventEmitter } from "eventemitter3";
/**
* Listen to Contract events in real time
@@ -28,7 +28,7 @@ export class ContractEvents<TContract extends BaseContract> {
* @param listener - the callback function that will be called on every transaction
* @public
*/
public addTransactionListener(listener: ListenerFn) {
public addTransactionListener(listener: EventEmitter.ListenerFn) {
this.contractWrapper.addListener(EventType.Transaction, listener);
}
@@ -44,7 +44,7 @@ export class ContractEvents<TContract extends BaseContract> {
* @param listener - the callback function to remove
* @public
*/
public removeTransactionListener(listener: ListenerFn) {
public removeTransactionListener(listener: EventEmitter.ListenerFn) {
this.contractWrapper.off(EventType.Transaction, listener);
}
@@ -1,4 +1,4 @@
import { hasFunction } from "../../common";
import { detectContractFeature } from "../../common";
import { normalizePriceValue, setErc20Allowance } from "../../common/currency";
import { uploadOrExtractURIs } from "../../common/nft";
import { FEATURE_NFT_SIGNATURE_MINTABLE_V2 } from "../../constants/erc721-features";
@@ -22,7 +22,7 @@ import type {
} from "@thirdweb-dev/contracts-js";
import { TokensMintedWithSignatureEvent } from "@thirdweb-dev/contracts-js/dist/declarations/src/SignatureDrop";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { BigNumber, ethers } from "ethers";
import { BigNumber } from "ethers";
import invariant from "tiny-invariant";
/**
@@ -397,17 +397,9 @@ export class Erc721WithQuantitySignatureMintable implements DetectableFeature {
}
private async isLegacyNFTContract() {
if (hasFunction<TokenERC721>("contractType", this.contractWrapper)) {
try {
const contractType = ethers.utils.toUtf8String(
await this.contractWrapper.readContract.contractType(),
);
return contractType.includes("TokenERC721");
} catch (e) {
return false;
}
} else {
return false;
}
return detectContractFeature<ISignatureMintERC721>(
this.contractWrapper,
"ERC721SignatureMintV1",
);
}
}
@@ -207,7 +207,7 @@ export class MarketplaceAuction {
validateNewListingParam(listing);
await handleTokenApproval(
this.contractWrapper.getSignerOrProvider(),
this.contractWrapper,
this.getAddress(),
listing.assetContractAddress,
listing.tokenId,
@@ -162,7 +162,7 @@ export class MarketplaceDirect {
validateNewListingParam(listing);
await handleTokenApproval(
this.contractWrapper.getSignerOrProvider(),
this.contractWrapper,
this.getAddress(),
listing.assetContractAddress,
listing.tokenId,
@@ -14,7 +14,7 @@ import EventEmitter from "eventemitter3";
export class RPCConnectionHandler extends EventEmitter {
private provider: providers.Provider;
private signer: Signer | undefined;
protected readonly options: SDKOptionsOutput;
public readonly options: SDKOptionsOutput;
constructor(network: NetworkOrSignerOrProvider, options: SDKOptions) {
super();
+114 -83
View File
@@ -1,99 +1,130 @@
import { providers, utils } from "ethers";
const DEFAULT_BATCH_TIME_LIMIT_MS = 50;
const DEFAULT_BATCH_SIZE_LIMIT = 250;
const DEFAULT_BATCH_OPTIONS = {
timeLimitMs: DEFAULT_BATCH_TIME_LIMIT_MS,
sizeLimit: DEFAULT_BATCH_SIZE_LIMIT,
};
export type BatchOptions = Partial<typeof DEFAULT_BATCH_OPTIONS>;
// mostly copied from ethers.js directly but make it a StaticJsonRpcProvider
export class StaticJsonRpcBatchProvider extends providers.StaticJsonRpcProvider {
_pendingBatchAggregator: NodeJS.Timer | null;
_pendingBatch: Array<{
request: { method: string, params: Array<any>, id: number, jsonrpc: "2.0" },
resolve: (result: any) => void,
reject: (error: Error) => void
}> | null;
private _timeLimitMs: number;
private _sizeLimit: number;
_pendingBatchAggregator: NodeJS.Timer | null;
_pendingBatch: Array<{
request: { method: string; params: Array<any>; id: number; jsonrpc: "2.0" };
resolve: (result: any) => void;
reject: (error: Error) => void;
}> | null;
constructor(url: string | utils.ConnectionInfo | undefined, network: providers.Networkish | undefined){
super(url, network);
this._pendingBatchAggregator = null;
this._pendingBatch = null;
constructor(
url: string | utils.ConnectionInfo | undefined,
network: providers.Networkish | undefined,
batchOptions: BatchOptions = DEFAULT_BATCH_OPTIONS,
) {
super(url, network);
this._timeLimitMs = batchOptions.timeLimitMs || DEFAULT_BATCH_SIZE_LIMIT;
this._sizeLimit = batchOptions.sizeLimit || DEFAULT_BATCH_TIME_LIMIT_MS;
this._pendingBatchAggregator = null;
this._pendingBatch = null;
}
private sendCurrentBatch(request: any) {
// if we still have a timeout clear that first
if (this._pendingBatchAggregator) {
clearTimeout(this._pendingBatchAggregator);
}
// Get the current batch and clear it, so new requests
// go into the next batch
const batch = this._pendingBatch || [];
this._pendingBatch = null;
this._pendingBatchAggregator = null;
send(method: string, params: Array<any>): Promise<any> {
const request = {
method: method,
params: params,
id: (this._nextId++),
jsonrpc: "2.0"
};
// Get the request as an array of requests
const request_ = batch.map((inflight) => inflight.request);
if (this._pendingBatch === null) {
this._pendingBatch = [ ];
}
this.emit("debug", {
action: "requestBatch",
request: utils.deepCopy(request),
provider: this,
});
const inflightRequest: any = { request, resolve: null, reject: null };
const promise = new Promise((resolve, reject) => {
inflightRequest.resolve = resolve;
inflightRequest.reject = reject;
return utils.fetchJson(this.connection, JSON.stringify(request_)).then(
(result) => {
this.emit("debug", {
action: "response",
request: request_,
response: result,
provider: this,
});
this._pendingBatch.push(inflightRequest);
// For each result, feed it to the correct Promise, depending
// on whether it was a success or error
batch.forEach((inflightRequest_, index) => {
const payload = result[index];
if (payload.error) {
const error = new Error(payload.error.message);
(error as any).code = payload.error.code;
(error as any).data = payload.error.data;
inflightRequest_.reject(error);
} else {
inflightRequest_.resolve(payload.result);
}
});
},
(error) => {
this.emit("debug", {
action: "response",
error: error,
request: request_,
provider: this,
});
if (!this._pendingBatchAggregator) {
// Schedule batch for next event loop + short duration
this._pendingBatchAggregator = setTimeout(() => {
// If there was an error, reject all the requests
batch.forEach((inflightRequest_) => {
inflightRequest_.reject(error);
});
},
);
}
// Get the current batch and clear it, so new requests
// go into the next batch
const batch = this._pendingBatch || [];
this._pendingBatch = null;
this._pendingBatchAggregator = null;
send(method: string, params: Array<any>): Promise<any> {
const request = {
method: method,
params: params,
id: this._nextId++,
jsonrpc: "2.0",
};
// Get the request as an array of requests
const request_ = batch.map((inflight) => inflight.request);
this.emit("debug", {
action: "requestBatch",
request: utils.deepCopy(request),
provider: this
});
return utils.fetchJson(this.connection, JSON.stringify(request_)).then((result) => {
this.emit("debug", {
action: "response",
request: request_,
response: result,
provider: this
});
// For each result, feed it to the correct Promise, depending
// on whether it was a success or error
batch.forEach((inflightRequest_, index) => {
const payload = result[index];
if (payload.error) {
const error = new Error(payload.error.message);
(<any>error).code = payload.error.code;
(<any>error).data = payload.error.data;
inflightRequest_.reject(error);
} else {
inflightRequest_.resolve(payload.result);
}
});
}, (error) => {
this.emit("debug", {
action: "response",
error: error,
request: request,
provider: this
});
batch.forEach((inflightRequest_) => {
inflightRequest_.reject(error);
});
});
}, 10);
}
return promise;
if (this._pendingBatch === null) {
this._pendingBatch = [];
}
}
const inflightRequest: any = { request, resolve: null, reject: null };
const promise = new Promise((resolve, reject) => {
inflightRequest.resolve = resolve;
inflightRequest.reject = reject;
});
// if we would go *over* the size limit of the batch with this request, send the batch now
if (this._pendingBatch.length === this._sizeLimit) {
this.sendCurrentBatch(request);
}
this._pendingBatch.push(inflightRequest);
if (!this._pendingBatchAggregator) {
// Schedule batch for next event loop + short duration
this._pendingBatchAggregator = setTimeout(() => {
this.sendCurrentBatch(request);
}, this._timeLimitMs);
}
return promise;
}
}
@@ -8,3 +8,4 @@ export * from "./token-erc20";
export * from "./token-erc721";
export * from "./token-erc1155";
export * from "./vote";
export * from "./custom";
@@ -0,0 +1,62 @@
import { AbstractWallet } from "../types/abstract";
import {
TypedDataDomain,
TypedDataField,
} from "@ethersproject/abstract-signer";
import { ethers } from "ethers";
import { AwsKmsSigner, AwsKmsSignerCredentials } from "ethers-aws-kms-signer";
/**
* Create a wallet instance using a private key stored in AWS KMS.
*
* @example
* ```javascript
* import { ThirdwebSDK } from "@thirdweb-dev/sdk"
* import { AwsKmsWallet } from "@thirdweb-dev/sdk/evm/wallets"
*
* const wallet = new AwsKmsallet({
* region: "us-east-1",
* accessKeyId: process.env.AWS_ACCESS_KEY_ID,
* secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
* sessionToken: process.env.AWS_SESSION_TOKEN,
* keyId: process.env.AWS_KEY_ID,
* });
*
* const sdk = await ThirdwebSDK.fromWallet(wallet, "mainnet");
* ```
*/
export class AwsKmsWallet extends AbstractWallet {
private cachedSigner: AwsKmsSigner;
constructor(options: AwsKmsSignerCredentials) {
super();
this.cachedSigner = this.updateSigner(new AwsKmsSigner(options));
}
async getSigner(
provider?: ethers.providers.Provider,
): Promise<ethers.Signer> {
if (provider) {
this.cachedSigner = this.updateSigner(
this.cachedSigner.connect(provider),
);
}
return this.cachedSigner;
}
// Add _signTypedData method onto the signer for now so we don't need to reimpliment
// The entire AWS KMS signer repository and maintain it ourselves.
private updateSigner(signer: AwsKmsSigner): AwsKmsSigner {
(signer as any)._signTypedData = async function (
domain: TypedDataDomain,
types: Record<string, Array<TypedDataField>>,
value: Record<string, any>,
) {
const hash = ethers.utils._TypedDataEncoder.hash(domain, types, value);
return signer._signDigest(hash);
};
return signer;
}
}
@@ -18,7 +18,7 @@ export type AwsSecretsManagerWalletOptions = {
* @example
* ```javascript
* import { ThirdwebSDK } from "@thirdweb-dev/sdk"
* import { AWSSecretsManagerWallet } from "@thirdweb-dev/sdk/evm/wallets"
* import { AwsSecretsManagerWallet } from "@thirdweb-dev/sdk/evm/wallets"
*
* const wallet = new AwsSecretsManagerWallet({
* secretName: "my-secret",
@@ -35,7 +35,7 @@ export type AwsSecretsManagerWalletOptions = {
* const sdk = await ThirdwebSDK.fromWallet(wallet, "mainnet");
* ```
*/
export class AWSSecretsManagerWallet extends AbstractWallet {
export class AwsSecretsManagerWallet extends AbstractWallet {
private secretId: string;
private secretKeyName: string;
private client: SecretsManagerClient;
+2 -1
View File
@@ -1 +1,2 @@
export * from "./aws";
export * from "./aws-secrets-manager";
export * from "./aws-kms";
@@ -9,7 +9,6 @@ import {
} from "../../constants/addresses";
import { TransactionResult } from "../../types/common";
import { CreatorOutput } from "../../types/programs";
import { getPublicRpc } from "../../utils/urls";
import { parseCreators } from "./creators-helper";
import {
findMasterEditionV2Pda,
@@ -96,15 +95,10 @@ export class NFTHelper {
async ownerOf(nftAddress: string): Promise<string | undefined> {
try {
// TODO switch back to normal connection when alchemy supports getTokenLargestAccounts
const connection = new Connection(
getPublicRpc(this.metaplex),
"confirmed",
);
const largestAccounts = await connection.getTokenLargestAccounts(
const largestAccounts = await this.connection.getTokenLargestAccounts(
new PublicKey(nftAddress),
);
const largestAccountInfo = await connection.getParsedAccountInfo(
const largestAccountInfo = await this.connection.getParsedAccountInfo(
largestAccounts.value[0].address,
);
const parsedData = largestAccountInfo?.value?.data as ParsedAccountData;
@@ -56,7 +56,16 @@ export type NFTCollectionMetadataInput = z.input<
*/
export const TokenMetadataInputSchema = CommonContractSchema.extend({
decimals: z.number().default(9),
initialSupply: AmountSchema,
initialSupply: AmountSchema.superRefine((val, context) => {
// TODO remove this limitation when metaplex fixes https://github.com/metaplex-foundation/js/issues/421
if (Number(val) > 9999999) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: `Initial supply must less than 10M, additional supply can be minted after deployment.`,
path: ["initialSupply"],
});
}
}),
});
/**
@@ -0,0 +1,67 @@
import {
extractMinimalProxyImplementationAddress,
ThirdwebSDK,
} from "../../src/evm";
import { signers, sdk } from "./before-setup";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { expect } from "chai";
global.fetch = require("cross-fetch");
describe("Custom Contracts", async () => {
let adminWallet: SignerWithAddress;
let realSDK: ThirdwebSDK;
before(async () => {
[adminWallet] = signers;
realSDK = new ThirdwebSDK(adminWallet);
});
beforeEach(async () => {
sdk.updateSignerOrProvider(adminWallet);
realSDK.updateSignerOrProvider(adminWallet);
});
it("should extract implementation address for eip-1167 minimal proxy", async () => {
// https://eips.ethereum.org/EIPS/eip-1167#specification
const code =
"0x363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3";
const implementationAddress =
extractMinimalProxyImplementationAddress(code);
expect(implementationAddress).to.equal(
"0xbebebebebebebebebebebebebebebebebebebebe",
);
});
it("should extract implementation address for minimal proxy with receive", async () => {
// https://github.com/0xSplits/splits-contracts/blob/c7b741926ec9746182d0d1e2c4c2046102e5d337/contracts/libraries/Clones.sol#L32
const code =
"0x36603057343d52307f830d2d700a97af574b186c80d40429385d24241565b08a7c559ba283a964d9b160203da23d3df35b3d3d3d3d363d3d37363d73bebebebebebebebebebebebebebebebebebebebe5af43d3d93803e605b57fd5bf3";
const implementationAddress =
extractMinimalProxyImplementationAddress(code);
expect(implementationAddress).to.equal(
"0xbebebebebebebebebebebebebebebebebebebebe",
);
});
it("should extract implementation address for more minimal proxy", async () => {
// https://medium.com/coinmonks/the-more-minimal-proxy-5756ae08ee48
const code =
"0x3d3d3d3d363d3d37363d73bebebebebebebebebebebebebebebebebebebebe5af43d3d93803e602a57fd5bf3";
const implementationAddress =
extractMinimalProxyImplementationAddress(code);
expect(implementationAddress).to.equal(
"0xbebebebebebebebebebebebebebebebebebebebe",
);
});
it("should extract implementation address for older vyper (uniswap v1) proxy", async () => {
const code =
"0x366000600037611000600036600073bebebebebebebebebebebebebebebebebebebebe5af41558576110006000f3";
const implementationAddress =
extractMinimalProxyImplementationAddress(code);
expect(implementationAddress).to.equal(
"0xbebebebebebebebebebebebebebebebebebebebe",
);
});
});
+4
View File
@@ -0,0 +1,4 @@
# folders
dist/
# files
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ["thirdweb"],
};
+37
View File
@@ -0,0 +1,37 @@
# @thirdweb-dev/unity-js-bridge
## 0.1.3
### Patch Changes
- Updated dependencies [[`3731459`](https://github.com/thirdweb-dev/js/commit/3731459d33f1ded7ebb69124809449b901b3ad3d), [`cac6c30`](https://github.com/thirdweb-dev/js/commit/cac6c30bca5e17df81d746ef81316af47d5e252e), [`7a37e56`](https://github.com/thirdweb-dev/js/commit/7a37e564fd5d5a9df84c8da44ecaf6c42f67a0e2)]:
- @thirdweb-dev/sdk@3.6.7
## 0.1.2
### Patch Changes
- Updated dependencies [[`8c6cdaa`](https://github.com/thirdweb-dev/js/commit/8c6cdaa2887fb2cc40d3ee6991233d195d103805)]:
- @thirdweb-dev/sdk@3.6.6
## 0.1.1
### Patch Changes
- [#433](https://github.com/thirdweb-dev/js/pull/433) [`7269268`](https://github.com/thirdweb-dev/js/commit/72692689e9adb41e92038765fd01a36c2b7457e1) Thanks [@jnsdls](https://github.com/jnsdls)! - add fundWallet method to bridge
- Updated dependencies [[`efc56fa`](https://github.com/thirdweb-dev/js/commit/efc56fa5802490ac8ef50037658d046afd89e9a1), [`7269268`](https://github.com/thirdweb-dev/js/commit/72692689e9adb41e92038765fd01a36c2b7457e1)]:
- @thirdweb-dev/sdk@3.6.5
- @thirdweb-dev/pay@0.1.0
## 0.1.0
### Minor Changes
- [#420](https://github.com/thirdweb-dev/js/pull/420) [`639e535`](https://github.com/thirdweb-dev/js/commit/639e535ed55280ad9d081001aab3f5af72bb3e45) Thanks [@jnsdls](https://github.com/jnsdls)! - Initial Release
### Patch Changes
- Updated dependencies [[`639e535`](https://github.com/thirdweb-dev/js/commit/639e535ed55280ad9d081001aab3f5af72bb3e45), [`f451da6`](https://github.com/thirdweb-dev/js/commit/f451da6395689a5f89800ee63f34b6175b61f703), [`af3acc6`](https://github.com/thirdweb-dev/js/commit/af3acc6b10751b840e56aef6400da5eea6040df2), [`1bfb91d`](https://github.com/thirdweb-dev/js/commit/1bfb91dbf3a39160c987a425813ac5dbb84703ad), [`ee47840`](https://github.com/thirdweb-dev/js/commit/ee478407673b0416e0c1cfe2be11bd6963395348), [`def4251`](https://github.com/thirdweb-dev/js/commit/def42511ff5a20d83f9094164dafb87e412571b5), [`034a257`](https://github.com/thirdweb-dev/js/commit/034a257442314c67729f7bafcbe3740cff33fa32), [`eca4776`](https://github.com/thirdweb-dev/js/commit/eca47763cd89cc3b7aa57b542971837987540b55), [`639e535`](https://github.com/thirdweb-dev/js/commit/639e535ed55280ad9d081001aab3f5af72bb3e45)]:
- @thirdweb-dev/wallets@0.1.0
- @thirdweb-dev/sdk@3.6.4
+16
View File
@@ -0,0 +1,16 @@
<p align="center">
<br />
<a href="https://thirdweb.com"><img src="https://github.com/thirdweb-dev/js/blob/main/packages/sdk/logo.svg?raw=true" width="200" alt=""/></a>
<br />
</p>
<h1 align="center">thirdweb Unity JS bridge</h1>
<p align="center">
<!-- <a href="https://www.npmjs.com/package/@thirdweb-dev/sdk"><img src="https://img.shields.io/npm/v/@thirdweb-dev/sdk?color=red&label=npm&logo=npm" alt="npm version"/></a> -->
<!-- <a href="https://github.com/thirdweb-dev/js/actions/workflows/CI.yml"><img alt="Build Status" src="https://github.com/thirdweb-dev/js/actions/workflows/CI.yml/badge.svg"/></a> -->
<!-- <a href="https://discord.gg/thirdweb"><img alt="Join our Discord!" src="https://img.shields.io/discord/834227967404146718.svg?color=7289da&label=discord&logo=discord&style=flat"/></a> -->
</p>
<!-- <p align="center"><strong>Best in class Web3 SDK for Browser, Node and Mobile apps</strong></p> -->
<br />
### todo
+34
View File
@@ -0,0 +1,34 @@
{
"name": "@thirdweb-dev/unity-js-bridge",
"version": "0.1.3",
"main": "dist/thirdweb-unity-bridge.js",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/unity-js-bridge",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/thirdweb-dev/js/issues"
},
"author": "thirdweb eng <[email protected]>",
"files": [
"dist/"
],
"dependencies": {
"@thirdweb-dev/sdk": "*",
"@thirdweb-dev/storage": "*",
"@thirdweb-dev/wallets": "*",
"@thirdweb-dev/pay": "*",
"ethers": "^5.7.2"
},
"devDependencies": {
"esbuild": "^0.15.16",
"node-stdlib-browser": "^1.2.0",
"typescript": "^4.7.4"
},
"scripts": {
"format": "prettier --write 'src/**/*'",
"lint": "eslint src/",
"fix": "eslint src/ --fix",
"clean": "rm -rf dist/",
"build": "yarn clean && tsc && node scripts/build.js",
"push": "yalc push"
}
}
+28
View File
@@ -0,0 +1,28 @@
const esbuild = require("esbuild");
const plugin = require("node-stdlib-browser/helpers/esbuild/plugin");
const stdLibBrowser = require("node-stdlib-browser");
// const DIST_PATH = path.resolve(process.cwd(), "dist/");
// fs.rmSync(DIST_PATH, { recursive: true, force: true });
esbuild.build({
jsx: "automatic",
entryPoints: ["src/thirdweb-bridge.ts"],
bundle: true,
minify: true,
platform: "browser",
target: "es2020",
outfile: "dist/thirdweb-unity-bridge.js",
splitting: false,
write: true,
sourcemap: false,
inject: [require.resolve("node-stdlib-browser/helpers/esbuild/shim")],
define: {
process: JSON.stringify({
env: "production",
}),
Buffer: "Buffer",
},
plugins: [plugin(stdLibBrowser)],
});
@@ -0,0 +1,234 @@
/// --- Thirdweb Brige ---
import { CoinbasePayIntegration, FundWalletOptions } from "@thirdweb-dev/pay";
import { ChainOrRpc, ThirdwebSDK } from "@thirdweb-dev/sdk/evm";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import {
CoinbaseWallet,
MetaMask,
WalletConnect,
InjectedWallet,
} from "@thirdweb-dev/wallets";
import type { AbstractWallet } from "@thirdweb-dev/wallets/dist/declarations/src/wallets/base";
import { BigNumber } from "ethers";
import type { ContractInterface, Signer } from "ethers";
declare global {
interface Window {
bridge: TWBridge;
}
}
const SEPARATOR = "/";
const SUB_SEPARATOR = "#";
// big number transform
const bigNumberReplacer = (_key: string, value: any) => {
// if we find a BigNumber then make it into a string (since that is safe)
if (
BigNumber.isBigNumber(value) ||
(typeof value === "object" &&
value !== null &&
value.type === "BigNumber" &&
"hex" in value)
) {
return BigNumber.from(value).toString();
}
return value;
};
const WALLETS = [
MetaMask,
InjectedWallet,
WalletConnect,
CoinbaseWallet,
] as const;
type PossibleWallet = typeof WALLETS[number]["id"];
type FundWalletInput = FundWalletOptions & {
appId: string;
};
interface TWBridge {
initialize: (chain: ChainOrRpc, options: string) => void;
connect: (wallet: PossibleWallet, chainId?: number) => Promise<string>;
disconnect: () => Promise<void>;
switchNetwork: (chainId: number) => Promise<void>;
invoke: (route: string, payload: string) => Promise<string | undefined>;
fundWallet: (options: string) => Promise<void>;
}
const w = window;
class ThirdwebBridge implements TWBridge {
private walletMap: Map<string, AbstractWallet> = new Map();
private activeWallet: AbstractWallet | undefined;
private initializedChain: ChainOrRpc | undefined;
private activeSDK: ThirdwebSDK | undefined;
private updateSDKSigner(signer?: Signer) {
if (this.activeSDK) {
if (signer) {
// set signer if we got one
this.activeSDK.updateSignerOrProvider(signer);
} else if (this.initializedChain) {
// reset back to provider only in case signer gets reomved (disconnect case)
this.activeSDK.updateSignerOrProvider(this.initializedChain);
}
}
}
public initialize(chain: ChainOrRpc, options: string) {
this.initializedChain = chain;
console.debug("thirdwebSDK initialization:", chain, options);
const sdkOptions = JSON.parse(options);
const storage =
sdkOptions && sdkOptions.ipfsGatewayUrl
? new ThirdwebStorage({
gatewayUrls: {
"ipfs://": [sdkOptions.ipfsGatewayUrl],
},
})
: new ThirdwebStorage();
this.activeSDK = new ThirdwebSDK(chain, sdkOptions, storage);
for (let wallet of WALLETS) {
const walletInstance = new wallet({
appName: sdkOptions.appName || "thirdweb powered dApp",
});
walletInstance.on("connect", async () =>
this.updateSDKSigner(await walletInstance.getSigner()),
);
walletInstance.on("change", async () =>
this.updateSDKSigner(await walletInstance.getSigner()),
);
walletInstance.on("disconnect", () => this.updateSDKSigner());
this.walletMap.set(wallet.id, walletInstance);
}
}
public async connect(
wallet: PossibleWallet = "injected",
chainId?: number | undefined,
) {
if (!this.activeSDK) {
throw new Error("SDK not initialized");
}
if (chainId === 0) {
chainId = undefined;
}
const walletInstance = this.walletMap.get(wallet);
if (walletInstance) {
await walletInstance.connect(chainId);
this.activeWallet = walletInstance;
this.updateSDKSigner(await walletInstance.getSigner());
return await this.activeSDK.wallet.getAddress();
} else {
throw new Error("Invalid Wallet");
}
}
public async disconnect() {
if (this.activeWallet) {
await this.activeWallet.disconnect();
this.activeWallet = undefined;
this.updateSDKSigner();
}
}
public async switchNetwork(chainId: number) {
if (chainId && this.activeWallet && "switchChain" in this.activeWallet) {
await this.activeWallet.switchChain(chainId);
this.updateSDKSigner(await this.activeWallet.getSigner());
} else {
throw new Error("Error Switching Network");
}
}
public async invoke(route: string, payload: string) {
if (!this.activeSDK) {
throw new Error("SDK not initialized");
}
const routeArgs = route.split(SEPARATOR);
const firstArg = routeArgs[0].split(SUB_SEPARATOR);
const addrOrSDK = firstArg[0];
const fnArgs = JSON.parse(payload).arguments;
const parsedArgs = fnArgs.map((arg: unknown) => {
try {
return typeof arg === "string" &&
(arg.startsWith("{") || arg.startsWith("["))
? JSON.parse(arg)
: arg;
} catch (e) {
return arg;
}
});
console.debug("thirdwebSDK call:", route, parsedArgs);
// wallet call
if (addrOrSDK.startsWith("sdk")) {
let prop = undefined;
if (firstArg.length > 1) {
prop = firstArg[1];
}
if (prop && routeArgs.length === 2) {
// @ts-expect-error need to type-guard this properly
const result = await this.activeSDK[prop][routeArgs[1]](...parsedArgs);
return JSON.stringify({ result: result }, bigNumberReplacer);
} else if (routeArgs.length === 2) {
// @ts-expect-error need to type-guard this properly
const result = await this.activeSDK[routeArgs[1]](...parsedArgs);
return JSON.stringify({ result: result }, bigNumberReplacer);
} else {
throw new Error("Invalid Route");
}
}
// contract call
if (addrOrSDK.startsWith("0x")) {
let typeOrAbi: string | ContractInterface | undefined;
if (firstArg.length > 1) {
try {
typeOrAbi = JSON.parse(firstArg[1]); // try to parse ABI
} catch (e) {
typeOrAbi = firstArg[1];
}
}
const contract = typeOrAbi
? await this.activeSDK.getContract(addrOrSDK, typeOrAbi)
: await this.activeSDK.getContract(addrOrSDK);
if (routeArgs.length === 2) {
// @ts-expect-error need to type-guard this properly
const result = await contract[routeArgs[1]](...parsedArgs);
return JSON.stringify({ result: result }, bigNumberReplacer);
} else if (routeArgs.length === 3) {
// @ts-expect-error need to type-guard this properly
const result = await contract[routeArgs[1]][routeArgs[2]](
...parsedArgs,
);
return JSON.stringify({ result: result }, bigNumberReplacer);
} else if (routeArgs.length === 4) {
// @ts-expect-error need to type-guard this properly
const result = await contract[routeArgs[1]][routeArgs[2]][routeArgs[3]](
...parsedArgs,
);
return JSON.stringify({ result: result }, bigNumberReplacer);
} else {
throw new Error("Invalid Route");
}
}
}
public async fundWallet(options: string) {
if (!this.activeSDK) {
throw new Error("SDK not initialized");
}
const { appId, ...fundOptions } = JSON.parse(options) as FundWalletInput;
const cbPay = new CoinbasePayIntegration({ appId });
return await cbPay.fundWallet(fundOptions);
}
}
// add the bridge to the window object type
w.bridge = new ThirdwebBridge();
/// --- End Thirdweb Brige ---
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "@thirdweb-dev/tsconfig/base.json",
"include": ["src", "types"],
"exclude": ["dist", "build", "node_modules"]
}
+6
View File
@@ -0,0 +1,6 @@
{
"presets": [
"@babel/preset-typescript",
["@babel/preset-env", { "targets": "defaults, not ie 11" }]
]
}
+4
View File
@@ -0,0 +1,4 @@
# folders
dist/
# files
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ["thirdweb"],
};
+7
View File
@@ -0,0 +1,7 @@
# @thirdweb-dev/wallets
## 0.1.0
### Minor Changes
- [#420](https://github.com/thirdweb-dev/js/pull/420) [`639e535`](https://github.com/thirdweb-dev/js/commit/639e535ed55280ad9d081001aab3f5af72bb3e45) Thanks [@jnsdls](https://github.com/jnsdls)! - Init wallet package
+16
View File
@@ -0,0 +1,16 @@
<p align="center">
<br />
<a href="https://thirdweb.com"><img src="https://github.com/thirdweb-dev/js/blob/main/packages/sdk/logo.svg?raw=true" width="200" alt=""/></a>
<br />
</p>
<h1 align="center">thirdweb wallets</h1>
<p align="center">
<!-- <a href="https://www.npmjs.com/package/@thirdweb-dev/sdk"><img src="https://img.shields.io/npm/v/@thirdweb-dev/sdk?color=red&label=npm&logo=npm" alt="npm version"/></a> -->
<!-- <a href="https://github.com/thirdweb-dev/js/actions/workflows/CI.yml"><img alt="Build Status" src="https://github.com/thirdweb-dev/js/actions/workflows/CI.yml/badge.svg"/></a> -->
<!-- <a href="https://discord.gg/thirdweb"><img alt="Join our Discord!" src="https://img.shields.io/discord/834227967404146718.svg?color=7289da&label=discord&logo=discord&style=flat"/></a> -->
</p>
<!-- <p align="center"><strong>Best in class Web3 SDK for Browser, Node and Mobile apps</strong></p> -->
<br />
### todo
@@ -0,0 +1,4 @@
{
"main": "dist/thirdweb-dev-wallets-connectors-coinbase-wallet.cjs.js",
"module": "dist/thirdweb-dev-wallets-connectors-coinbase-wallet.esm.js"
}
@@ -0,0 +1,4 @@
{
"main": "dist/thirdweb-dev-wallets-connectors-injected.cjs.js",
"module": "dist/thirdweb-dev-wallets-connectors-injected.esm.js"
}
@@ -0,0 +1,4 @@
{
"main": "dist/thirdweb-dev-wallets-connectors-metamask.cjs.js",
"module": "dist/thirdweb-dev-wallets-connectors-metamask.esm.js"
}
@@ -0,0 +1,4 @@
{
"main": "dist/thirdweb-dev-wallets-connectors-wallet-connect.cjs.js",
"module": "dist/thirdweb-dev-wallets-connectors-wallet-connect.esm.js"
}
+92
View File
@@ -0,0 +1,92 @@
{
"name": "@thirdweb-dev/wallets",
"version": "0.1.0",
"main": "dist/thirdweb-dev-wallets.cjs.js",
"module": "dist/thirdweb-dev-wallets.esm.js",
"exports": {
".": {
"module": "./dist/thirdweb-dev-wallets.esm.js",
"default": "./dist/thirdweb-dev-wallets.cjs.js"
},
"./wallets/base": {
"module": "./wallets/base/dist/thirdweb-dev-wallets-wallets-base.esm.js",
"default": "./wallets/base/dist/thirdweb-dev-wallets-wallets-base.cjs.js"
},
"./wallets/injected": {
"module": "./wallets/injected/dist/thirdweb-dev-wallets-wallets-injected.esm.js",
"default": "./wallets/injected/dist/thirdweb-dev-wallets-wallets-injected.cjs.js"
},
"./wallets/metamask": {
"module": "./wallets/metamask/dist/thirdweb-dev-wallets-wallets-metamask.esm.js",
"default": "./wallets/metamask/dist/thirdweb-dev-wallets-wallets-metamask.cjs.js"
},
"./wallets/wallet-connect": {
"module": "./wallets/wallet-connect/dist/thirdweb-dev-wallets-wallets-wallet-connect.esm.js",
"default": "./wallets/wallet-connect/dist/thirdweb-dev-wallets-wallets-wallet-connect.cjs.js"
},
"./wallets/coinbase-wallet": {
"module": "./wallets/coinbase-wallet/dist/thirdweb-dev-wallets-wallets-coinbase-wallet.esm.js",
"default": "./wallets/coinbase-wallet/dist/thirdweb-dev-wallets-wallets-coinbase-wallet.cjs.js"
},
"./connectors/injected": {
"module": "./connectors/injected/dist/thirdweb-dev-wallets-connectors-injected.esm.js",
"default": "./connectors/injected/dist/thirdweb-dev-wallets-connectors-injected.cjs.js"
},
"./connectors/metamask": {
"module": "./connectors/metamask/dist/thirdweb-dev-wallets-connectors-metamask.esm.js",
"default": "./connectors/metamask/dist/thirdweb-dev-wallets-connectors-metamask.cjs.js"
},
"./connectors/wallet-connect": {
"module": "./connectors/wallet-connect/dist/thirdweb-dev-wallets-connectors-wallet-connect.esm.js",
"default": "./connectors/wallet-connect/dist/thirdweb-dev-wallets-connectors-wallet-connect.cjs.js"
},
"./connectors/coinbase-wallet": {
"module": "./connectors/coinbase-wallet/dist/thirdweb-dev-wallets-connectors-coinbase-wallet.esm.js",
"default": "./connectors/coinbase-wallet/dist/thirdweb-dev-wallets-connectors-coinbase-wallet.cjs.js"
},
"./package.json": "./package.json"
},
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/wallets",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/thirdweb-dev/js/issues"
},
"author": "thirdweb eng <[email protected]>",
"files": [
"dist/",
"wallets/",
"connectors/"
],
"preconstruct": {
"entrypoints": [
"index.ts",
"connectors/**",
"wallets/**"
],
"___experimentalFlags_WILL_CHANGE_IN_PATCH": {
"exports": true
},
"exports": true
},
"sideEffects": false,
"dependencies": {
"@coinbase/wallet-sdk": "^3.6.0",
"@wagmi/core": "^0.7.6",
"@walletconnect/ethereum-provider": "^1.7.8",
"abitype": "^0.2.5",
"ethers": "^5.7.2",
"eventemitter3": "^5.0.0",
"localforage": "^1.10.0"
},
"devDependencies": {
"@preconstruct/cli": "^2.2.1",
"typescript": "^4.7.4"
},
"scripts": {
"format": "prettier --write 'src/**/*'",
"lint": "eslint src/",
"fix": "eslint src/ --fix",
"clean": "rm -rf dist/",
"build": "tsc && preconstruct build"
}
}
@@ -0,0 +1,289 @@
import type {
CoinbaseWalletProvider,
CoinbaseWalletSDK,
} from "@coinbase/wallet-sdk";
import type { CoinbaseWalletSDKOptions } from "@coinbase/wallet-sdk/dist/CoinbaseWalletSDK";
import {
AddChainError,
Chain,
ChainNotConfiguredError,
Connector,
normalizeChainId,
ProviderRpcError,
SwitchChainError,
UserRejectedRequestError,
} from "@wagmi/core";
import type { Address } from "abitype";
import { providers } from "ethers";
import { getAddress, hexValue } from "ethers/lib/utils.js";
type Options = CoinbaseWalletSDKOptions & {
/**
* Fallback Ethereum JSON RPC URL
* @default ""
*/
jsonRpcUrl?: string;
/**
* Fallback Ethereum Chain ID
* @default 1
*/
chainId?: number;
};
export class CoinbaseWalletConnector extends Connector<
CoinbaseWalletProvider,
Options,
providers.JsonRpcSigner
> {
readonly id = "coinbaseWallet";
readonly name = "Coinbase Wallet";
readonly ready = true;
#client?: CoinbaseWalletSDK;
#provider?: CoinbaseWalletProvider;
constructor({ chains, options }: { chains?: Chain[]; options: Options }) {
super({
chains,
options: {
reloadOnDisconnect: false,
...options,
},
});
}
async connect({ chainId }: { chainId?: number } = {}) {
try {
const provider = await this.getProvider();
provider.on("accountsChanged", this.onAccountsChanged);
provider.on("chainChanged", this.onChainChanged);
provider.on("disconnect", this.onDisconnect);
this.emit("message", { type: "connecting" });
const accounts = await provider.enable();
const account = getAddress(accounts[0] as string);
// Switch to chain if provided
let id = await this.getChainId();
let unsupported = this.isChainUnsupported(id);
if (chainId && id !== chainId) {
const chain = await this.switchChain(chainId);
id = chain.id;
unsupported = this.isChainUnsupported(id);
}
return {
account,
chain: { id, unsupported },
provider: new providers.Web3Provider(
provider as unknown as providers.ExternalProvider,
),
};
} catch (error) {
if (
/(user closed modal|accounts received is empty)/i.test(
(error as ProviderRpcError).message,
)
) {
throw new UserRejectedRequestError(error);
}
throw error;
}
}
async disconnect() {
if (!this.#provider) {
return;
}
const provider = await this.getProvider();
provider.removeListener("accountsChanged", this.onAccountsChanged);
provider.removeListener("chainChanged", this.onChainChanged);
provider.removeListener("disconnect", this.onDisconnect);
provider.disconnect();
provider.close();
}
async getAccount() {
const provider = await this.getProvider();
const accounts = await provider.request<Address[]>({
method: "eth_accounts",
});
// return checksum address
return getAddress(accounts[0] as string);
}
async getChainId() {
const provider = await this.getProvider();
const chainId = normalizeChainId(provider.chainId);
return chainId;
}
async getProvider() {
if (!this.#provider) {
let CoinbaseWalletSDK = (await import("@coinbase/wallet-sdk")).default;
// Workaround for Vite dev import errors
// https://github.com/vitejs/vite/issues/7112
if (
typeof CoinbaseWalletSDK !== "function" &&
// @ts-expect-error This import error is not visible to TypeScript
typeof CoinbaseWalletSDK.default === "function"
) {
CoinbaseWalletSDK = (
CoinbaseWalletSDK as unknown as { default: typeof CoinbaseWalletSDK }
).default;
}
this.#client = new CoinbaseWalletSDK(this.options);
/**
* Mock implementations to retrieve private `walletExtension` method
* from the Coinbase Wallet SDK.
*/
abstract class WalletProvider {
// https://github.com/coinbase/coinbase-wallet-sdk/blob/b4cca90022ffeb46b7bbaaab9389a33133fe0844/packages/wallet-sdk/src/provider/CoinbaseWalletProvider.ts#L927-L936
abstract getChainId(): number;
}
abstract class Client {
// https://github.com/coinbase/coinbase-wallet-sdk/blob/b4cca90022ffeb46b7bbaaab9389a33133fe0844/packages/wallet-sdk/src/CoinbaseWalletSDK.ts#L233-L235
abstract get walletExtension(): WalletProvider | undefined;
}
const walletExtensionChainId = (
this.#client as unknown as Client
).walletExtension?.getChainId();
const chain =
this.chains.find((chain_) =>
this.options.chainId
? chain_.id === this.options.chainId
: chain_.id === walletExtensionChainId,
) || this.chains[0];
const chainId = this.options.chainId || chain?.id;
const jsonRpcUrl = this.options.jsonRpcUrl || chain?.rpcUrls.default;
this.#provider = this.#client.makeWeb3Provider(jsonRpcUrl, chainId);
}
return this.#provider;
}
async getSigner({ chainId }: { chainId?: number } = {}) {
const [provider, account] = await Promise.all([
this.getProvider(),
this.getAccount(),
]);
return new providers.Web3Provider(
provider as unknown as providers.ExternalProvider,
chainId,
).getSigner(account);
}
async isAuthorized() {
try {
const account = await this.getAccount();
return !!account;
} catch {
return false;
}
}
async switchChain(chainId: number) {
const provider = await this.getProvider();
const id = hexValue(chainId);
try {
await provider.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: id }],
});
return (
this.chains.find((x) => x.id === chainId) ?? {
id: chainId,
name: `Chain ${id}`,
network: `${id}`,
rpcUrls: { default: "" },
}
);
} catch (error) {
const chain = this.chains.find((x) => x.id === chainId);
if (!chain) {
throw new ChainNotConfiguredError({ chainId, connectorId: this.id });
}
// Indicates chain is not added to provider
if ((error as ProviderRpcError).code === 4902) {
try {
await provider.request({
method: "wallet_addEthereumChain",
params: [
{
chainId: id,
chainName: chain.name,
nativeCurrency: chain.nativeCurrency,
rpcUrls: [chain.rpcUrls.public ?? chain.rpcUrls.default],
blockExplorerUrls: this.getBlockExplorerUrls(chain),
},
],
});
return chain;
} catch (addError) {
if (this.#isUserRejectedRequestError(addError)) {
throw new UserRejectedRequestError(addError);
}
throw new AddChainError();
}
}
if (this.#isUserRejectedRequestError(error)) {
throw new UserRejectedRequestError(error);
}
throw new SwitchChainError(error);
}
}
async watchAsset({
address,
decimals = 18,
image,
symbol,
}: {
address: string;
decimals?: number;
image?: string;
symbol: string;
}) {
const provider = await this.getProvider();
return provider.request<boolean>({
method: "wallet_watchAsset",
params: {
type: "ERC20",
options: {
address,
decimals,
image,
symbol,
},
},
});
}
protected onAccountsChanged = (accounts: string[]) => {
if (accounts.length === 0) {
this.emit("disconnect");
} else {
this.emit("change", { account: getAddress(accounts[0] as string) });
}
};
protected onChainChanged = (chainId: number | string) => {
const id = normalizeChainId(chainId);
const unsupported = this.isChainUnsupported(id);
this.emit("change", { chain: { id, unsupported } });
};
protected onDisconnect = () => {
this.emit("disconnect");
};
#isUserRejectedRequestError(error: unknown) {
return /(user rejected)/i.test((error as Error).message);
}
}
@@ -0,0 +1,340 @@
import { getInjectedName } from "../../utils/getInjectedName";
import { getConnectorStorage } from "../../utils/storage";
import {
AddChainError,
Chain,
ChainNotConfiguredError,
Connector,
ConnectorNotFoundError,
Ethereum,
normalizeChainId,
ProviderRpcError,
ResourceUnavailableError,
RpcError,
SwitchChainError,
UserRejectedRequestError,
} from "@wagmi/core";
import type { Address } from "abitype";
import { providers } from "ethers";
import { getAddress, hexValue } from "ethers/lib/utils.js";
declare interface Window {
ethereum: Ethereum;
}
export type InjectedConnectorOptions = {
/** Name of connector */
name?: string | ((detectedName: string | string[]) => string);
/**
* MetaMask 10.9.3 emits disconnect event when chain is changed.
* This flag prevents the `"disconnect"` event from being emitted upon switching chains. See [GitHub issue](https://github.com/MetaMask/metamask-extension/issues/13375#issuecomment-1027663334) for more info.
*/
shimChainChangedDisconnect?: boolean;
/**
* MetaMask and other injected providers do not support programmatic disconnect.
* This flag simulates the disconnect behavior by keeping track of connection status in storage. See [GitHub issue](https://github.com/MetaMask/metamask-extension/issues/10353) for more info.
* @default true
*/
shimDisconnect?: boolean;
};
export class InjectedConnector extends Connector<
Window["ethereum"],
InjectedConnectorOptions | undefined,
providers.JsonRpcSigner
> {
readonly id: string;
readonly name: string;
readonly ready = typeof window !== "undefined" && !!window.ethereum;
#provider?: Window["ethereum"];
#switchingChains?: boolean;
protected shimDisconnectKey = "injected.shimDisconnect";
constructor({
chains,
options: options_,
}: {
chains?: Chain[];
options?: InjectedConnectorOptions;
} = {}) {
const options = {
shimDisconnect: true,
shimChainChangedDisconnect: true,
...options_,
};
super({ chains, options });
let name = "Injected";
const overrideName = options.name;
if (typeof overrideName === "string") {
name = overrideName;
} else if (typeof window !== "undefined") {
const detectedName = getInjectedName(window.ethereum as Ethereum);
if (overrideName) {
name = overrideName(detectedName);
} else {
name =
typeof detectedName === "string"
? detectedName
: (detectedName[0] as string);
}
}
this.id = "injected";
this.name = name;
}
async connect({ chainId }: { chainId?: number } = {}) {
try {
const provider = await this.getProvider();
if (!provider) {
throw new ConnectorNotFoundError();
}
if (provider.on) {
provider.on("accountsChanged", this.onAccountsChanged);
provider.on("chainChanged", this.onChainChanged);
provider.on("disconnect", this.onDisconnect);
}
this.emit("message", { type: "connecting" });
const accounts = await provider.request({
method: "eth_requestAccounts",
});
const account = getAddress(accounts[0] as string);
// Switch to chain if provided
let id = await this.getChainId();
let unsupported = this.isChainUnsupported(id);
if (chainId && id !== chainId) {
const chain = await this.switchChain(chainId);
id = chain.id;
unsupported = this.isChainUnsupported(id);
}
// Add shim to storage signalling wallet is connected
if (this.options?.shimDisconnect) {
await getConnectorStorage().setItem(this.shimDisconnectKey, true);
}
return { account, chain: { id, unsupported }, provider };
} catch (error) {
if (this.isUserRejectedRequestError(error)) {
throw new UserRejectedRequestError(error);
}
if ((error as RpcError).code === -32002) {
throw new ResourceUnavailableError(error);
}
throw error;
}
}
async disconnect() {
const provider = await this.getProvider();
if (!provider?.removeListener) {
return;
}
provider.removeListener("accountsChanged", this.onAccountsChanged);
provider.removeListener("chainChanged", this.onChainChanged);
provider.removeListener("disconnect", this.onDisconnect);
// Remove shim signalling wallet is disconnected
if (this.options?.shimDisconnect) {
await getConnectorStorage().removeItem(this.shimDisconnectKey);
}
}
async getAccount() {
const provider = await this.getProvider();
if (!provider) {
throw new ConnectorNotFoundError();
}
const accounts = await provider.request({
method: "eth_accounts",
});
// return checksum address
return getAddress(accounts[0] as string);
}
async getChainId() {
const provider = await this.getProvider();
if (!provider) {
throw new ConnectorNotFoundError();
}
return provider.request({ method: "eth_chainId" }).then(normalizeChainId);
}
async getProvider() {
if (typeof window !== "undefined" && !!window.ethereum) {
this.#provider = window.ethereum as Ethereum;
}
return this.#provider as Ethereum;
}
async getSigner({ chainId }: { chainId?: number } = {}) {
const [provider, account] = await Promise.all([
this.getProvider(),
this.getAccount(),
]);
return new providers.Web3Provider(
provider as providers.ExternalProvider,
chainId,
).getSigner(account);
}
async isAuthorized() {
try {
if (
this.options?.shimDisconnect &&
// If shim does not exist in storage, wallet is disconnected
!(await getConnectorStorage().getItem(this.shimDisconnectKey))
) {
return false;
}
const provider = await this.getProvider();
if (!provider) {
throw new ConnectorNotFoundError();
}
const account = await this.getAccount();
return !!account;
} catch {
return false;
}
}
async switchChain(chainId: number) {
if (this.options?.shimChainChangedDisconnect) {
this.#switchingChains = true;
}
const provider = await this.getProvider();
if (!provider) {
throw new ConnectorNotFoundError();
}
const id = hexValue(chainId);
try {
await provider.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: id }],
});
return (
this.chains.find((x) => x.id === chainId) ?? {
id: chainId,
name: `Chain ${id}`,
network: `${id}`,
rpcUrls: { default: "" },
}
);
} catch (error) {
const chain = this.chains.find((x) => x.id === chainId);
if (!chain) {
throw new ChainNotConfiguredError({ chainId, connectorId: this.id });
}
// Indicates chain is not added to provider
if (
(error as ProviderRpcError).code === 4902 ||
// Unwrapping for MetaMask Mobile
// https://github.com/MetaMask/metamask-mobile/issues/2944#issuecomment-976988719
(error as RpcError<{ originalError?: { code: number } }>)?.data
?.originalError?.code === 4902
) {
try {
await provider.request({
method: "wallet_addEthereumChain",
params: [
{
chainId: id,
chainName: chain.name,
nativeCurrency: chain.nativeCurrency,
rpcUrls: [chain.rpcUrls.public ?? chain.rpcUrls.default],
blockExplorerUrls: this.getBlockExplorerUrls(chain),
},
],
});
return chain;
} catch (addError) {
if (this.isUserRejectedRequestError(addError)) {
throw new UserRejectedRequestError(error);
}
throw new AddChainError();
}
}
if (this.isUserRejectedRequestError(error)) {
throw new UserRejectedRequestError(error);
}
throw new SwitchChainError(error);
}
}
async watchAsset({
address,
decimals = 18,
image,
symbol,
}: {
address: Address;
decimals?: number;
image?: string;
symbol: string;
}) {
const provider = await this.getProvider();
if (!provider) {
throw new ConnectorNotFoundError();
}
return provider.request({
method: "wallet_watchAsset",
params: {
type: "ERC20",
options: {
address,
decimals,
image,
symbol,
},
},
});
}
protected onAccountsChanged = (accounts: string[]) => {
if (accounts.length === 0) {
this.emit("disconnect");
} else {
this.emit("change", {
account: getAddress(accounts[0] as string),
});
}
};
protected onChainChanged = (chainId: number | string) => {
const id = normalizeChainId(chainId);
const unsupported = this.isChainUnsupported(id);
this.emit("change", { chain: { id, unsupported } });
};
protected onDisconnect = async () => {
// We need this as MetaMask can emit the "disconnect" event
// upon switching chains. This workaround ensures that the
// user currently isn't in the process of switching chains.
if (this.options?.shimChainChangedDisconnect && this.#switchingChains) {
this.#switchingChains = false;
return;
}
this.emit("disconnect");
// Remove shim signalling wallet is disconnected
if (this.options?.shimDisconnect) {
await getConnectorStorage().removeItem(this.shimDisconnectKey);
}
};
protected isUserRejectedRequestError(error: unknown) {
return (error as ProviderRpcError).code === 4001;
}
}
@@ -0,0 +1,168 @@
import { getConnectorStorage } from "../../utils/storage";
import { InjectedConnector } from "../injected";
import {
Chain,
ConnectorNotFoundError,
Ethereum,
InjectedConnectorOptions,
ResourceUnavailableError,
RpcError,
UserRejectedRequestError,
} from "@wagmi/core";
import type { Address } from "abitype";
import { getAddress } from "ethers/lib/utils.js";
declare interface Window {
ethereum: Ethereum;
}
export type MetaMaskConnectorOptions = Pick<
InjectedConnectorOptions,
"shimChainChangedDisconnect" | "shimDisconnect"
> & {
/**
* While "disconnected" with `shimDisconnect`, allows user to select a different MetaMask account (than the currently connected account) when trying to connect.
*/
UNSTABLE_shimOnConnectSelectAccount?: boolean;
};
export class MetaMaskConnector extends InjectedConnector {
readonly id = "metaMask";
readonly ready =
typeof window !== "undefined" &&
!!this.#findProvider(window.ethereum as Ethereum);
#provider?: Window["ethereum"];
#UNSTABLE_shimOnConnectSelectAccount: MetaMaskConnectorOptions["UNSTABLE_shimOnConnectSelectAccount"];
constructor({
chains,
options: options_,
}: {
chains?: Chain[];
options?: MetaMaskConnectorOptions;
} = {}) {
const options = {
name: "MetaMask",
shimDisconnect: true,
shimChainChangedDisconnect: true,
...options_,
};
super({ chains, options });
this.#UNSTABLE_shimOnConnectSelectAccount =
options.UNSTABLE_shimOnConnectSelectAccount;
}
async connect({ chainId }: { chainId?: number } = {}) {
try {
const provider = await this.getProvider();
if (!provider) {
throw new ConnectorNotFoundError();
}
if (provider.on) {
provider.on("accountsChanged", this.onAccountsChanged);
provider.on("chainChanged", this.onChainChanged);
provider.on("disconnect", this.onDisconnect);
}
this.emit("message", { type: "connecting" });
// Attempt to show wallet select prompt with `wallet_requestPermissions` when
// `shimDisconnect` is active and account is in disconnected state (flag in storage)
let account: Address | null = null;
if (
this.#UNSTABLE_shimOnConnectSelectAccount &&
this.options?.shimDisconnect &&
(await getConnectorStorage().getItem(this.shimDisconnectKey))
) {
account = await this.getAccount().catch(() => null);
const isConnected = !!account;
if (isConnected) {
// Attempt to show another prompt for selecting wallet if already connected
await provider
.request({
method: "wallet_requestPermissions",
params: [{ eth_accounts: {} }],
})
.catch(() => null);
}
}
if (!account) {
const accounts = await provider.request({
method: "eth_requestAccounts",
});
account = getAddress(accounts[0] as string);
}
// Switch to chain if provided
let id = await this.getChainId();
let unsupported = this.isChainUnsupported(id);
if (chainId && id !== chainId) {
const chain = await this.switchChain(chainId);
id = chain.id;
unsupported = this.isChainUnsupported(id);
}
if (this.options?.shimDisconnect) {
await getConnectorStorage().setItem(this.shimDisconnectKey, true);
}
return { account, chain: { id, unsupported }, provider };
} catch (error) {
if (this.isUserRejectedRequestError(error)) {
throw new UserRejectedRequestError(error);
}
if ((error as RpcError).code === -32002) {
throw new ResourceUnavailableError(error);
}
throw error;
}
}
async getProvider() {
if (typeof window !== "undefined") {
// TODO: Fallback to `ethereum#initialized` event for async injection
// https://github.com/MetaMask/detect-provider#synchronous-and-asynchronous-injection=
this.#provider = this.#findProvider(window.ethereum as Ethereum);
}
return this.#provider as Ethereum;
}
#getReady(ethereum?: Ethereum) {
const isMetaMask = !!ethereum?.isMetaMask;
if (!isMetaMask) {
return;
}
// Brave tries to make itself look like MetaMask
// Could also try RPC `web3_clientVersion` if following is unreliable
if (ethereum.isBraveWallet && !ethereum._events && !ethereum._state) {
return;
}
if (ethereum.isAvalanche) {
return;
}
if (ethereum.isKuCoinWallet) {
return;
}
if (ethereum.isPortal) {
return;
}
if (ethereum.isTokenPocket) {
return;
}
if (ethereum.isTokenary) {
return;
}
return ethereum;
}
#findProvider(ethereum?: Ethereum) {
if (ethereum?.providers) {
return ethereum.providers.find(this.#getReady);
}
return this.#getReady(ethereum);
}
}
@@ -0,0 +1,216 @@
import {
Chain,
Connector,
normalizeChainId,
ProviderRpcError,
SwitchChainError,
UserRejectedRequestError,
} from "@wagmi/core";
import type WalletConnectProvider from "@walletconnect/ethereum-provider";
import { providers } from "ethers";
import { getAddress, hexValue } from "ethers/lib/utils.js";
/**
* Wallets that support chain switching through WalletConnect
* - imToken (token.im)
* - MetaMask (metamask.io)
* - Rainbow (rainbow.me)
* - Trust Wallet (trustwallet.com)
*/
const switchChainAllowedRegex = /(imtoken|metamask|rainbow|trust wallet)/i;
type WalletConnectOptions = ConstructorParameters<
typeof WalletConnectProvider
>[0];
type WalletConnectSigner = providers.JsonRpcSigner;
export class WalletConnectConnector extends Connector<
WalletConnectProvider,
WalletConnectOptions,
WalletConnectSigner
> {
readonly id = "walletConnect";
readonly name = "WalletConnect";
readonly ready = true;
#provider?: WalletConnectProvider;
constructor(config: { chains?: Chain[]; options: WalletConnectOptions }) {
super(config);
}
async connect({ chainId }: { chainId?: number } = {}) {
try {
let targetChainId = chainId || 1;
const provider = await this.getProvider({
chainId: targetChainId,
create: true,
});
provider.on("accountsChanged", this.onAccountsChanged);
provider.on("chainChanged", this.onChainChanged);
provider.on("disconnect", this.onDisconnect);
// Defer message to the next tick to ensure wallet connect data (provided by `.enable()`) is available
setTimeout(() => this.emit("message", { type: "connecting" }), 0);
const accounts = await provider.enable();
const account = getAddress(accounts[0] as string);
const id = await this.getChainId();
const unsupported = this.isChainUnsupported(id);
// Not all WalletConnect options support programmatic chain switching
// Only enable for wallet options that do
const walletName = provider.connector?.peerMeta?.name ?? "";
if (switchChainAllowedRegex.test(walletName)) {
this.switchChain = this.#switchChain;
}
return {
account,
chain: { id, unsupported },
provider: new providers.Web3Provider(
provider as providers.ExternalProvider,
),
};
} catch (error) {
if (/user closed modal/i.test((error as ProviderRpcError).message)) {
throw new UserRejectedRequestError(error);
}
throw error;
}
}
async disconnect() {
const provider = await this.getProvider();
await provider.disconnect();
provider.removeListener("accountsChanged", this.onAccountsChanged);
provider.removeListener("chainChanged", this.onChainChanged);
provider.removeListener("disconnect", this.onDisconnect);
if (typeof localStorage !== "undefined") {
localStorage.removeItem("walletconnect");
}
}
async getAccount() {
const provider = await this.getProvider();
const accounts = provider.accounts;
// return checksum address
return getAddress(accounts[0] as string);
}
async getChainId() {
const provider = await this.getProvider();
const chainId = normalizeChainId(provider.chainId);
return chainId;
}
async getProvider({
chainId,
create,
}: { chainId?: number; create?: boolean } = {}) {
// Force create new provider
if (!this.#provider || chainId || create) {
const rpc = !this.options?.infuraId
? this.chains.reduce(
(rpc_, chain) => ({ ...rpc_, [chain.id]: chain.rpcUrls.default }),
{},
)
: {};
const WalletConnectProvider = (
await import("@walletconnect/ethereum-provider")
).default;
this.#provider = new WalletConnectProvider({
...this.options,
chainId,
rpc: { ...rpc, ...this.options?.rpc },
});
}
return this.#provider;
}
async getSigner({ chainId }: { chainId?: number } = {}) {
const [provider, account] = await Promise.all([
this.getProvider({ chainId }),
this.getAccount(),
]);
return new providers.Web3Provider(
provider as providers.ExternalProvider,
chainId,
).getSigner(account);
}
async isAuthorized() {
try {
const account = await this.getAccount();
return !!account;
} catch {
return false;
}
}
async #switchChain(chainId: number) {
const provider = await this.getProvider();
const id = hexValue(chainId);
try {
// Set up a race between `wallet_switchEthereumChain` & the `chainChanged` event
// to ensure the chain has been switched. This is because there could be a case
// where a wallet may not resolve the `wallet_switchEthereumChain` method, or
// resolves slower than `chainChanged`.
await Promise.race([
provider.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: id }],
}),
new Promise((res) =>
this.on("change", ({ chain }) => {
if (chain?.id === chainId) {
res(chainId);
}
}),
),
]);
return (
this.chains.find((x) => x.id === chainId) ?? {
id: chainId,
name: `Chain ${id}`,
network: `${id}`,
rpcUrls: { default: "" },
}
);
} catch (error) {
const message =
typeof error === "string"
? error
: (error as ProviderRpcError)?.message;
if (/user rejected request/i.test(message)) {
throw new UserRejectedRequestError(error);
}
throw new SwitchChainError(error);
}
}
protected onAccountsChanged = (accounts: string[]) => {
if (accounts.length === 0) {
this.emit("disconnect");
} else {
this.emit("change", { account: getAddress(accounts[0] as string) });
}
};
protected onChainChanged = (chainId: number | string) => {
const id = normalizeChainId(chainId);
const unsupported = this.isChainUnsupported(id);
this.emit("change", { chain: { id, unsupported } });
};
protected onDisconnect = () => {
this.emit("disconnect");
};
}
+408
View File
@@ -0,0 +1,408 @@
import {
Chain,
etherscanBlockExplorers,
alchemyRpcUrls,
infuraRpcUrls,
} from "@wagmi/core";
/**
* @public
*/
export enum ChainId {
Mainnet = 1,
Goerli = 5,
Polygon = 137,
Mumbai = 80001,
Fantom = 250,
FantomTestnet = 4002,
Avalanche = 43114,
AvalancheFujiTestnet = 43113,
Optimism = 10,
OptimismGoerli = 420,
Arbitrum = 42161,
ArbitrumGoerli = 421613,
BinanceSmartChainMainnet = 56,
BinanceSmartChainTestnet = 97,
}
/**
* @public
*/
export type SUPPORTED_CHAIN_ID =
| ChainId.Mainnet
| ChainId.Goerli
| ChainId.Mumbai
| ChainId.Polygon
| ChainId.Fantom
| ChainId.FantomTestnet
| ChainId.Avalanche
| ChainId.AvalancheFujiTestnet
| ChainId.Optimism
| ChainId.OptimismGoerli
| ChainId.Arbitrum
| ChainId.ArbitrumGoerli
| ChainId.BinanceSmartChainMainnet
| ChainId.BinanceSmartChainTestnet;
/**
* @public
*/
export const SUPPORTED_CHAIN_IDS: SUPPORTED_CHAIN_ID[] = [
ChainId.Mainnet,
ChainId.Goerli,
ChainId.Polygon,
ChainId.Mumbai,
ChainId.Fantom,
ChainId.FantomTestnet,
ChainId.Avalanche,
ChainId.AvalancheFujiTestnet,
ChainId.Optimism,
ChainId.OptimismGoerli,
ChainId.Arbitrum,
ChainId.ArbitrumGoerli,
ChainId.BinanceSmartChainMainnet,
ChainId.BinanceSmartChainTestnet,
];
type NativeCurrency = {
name: string;
/** 2-6 characters long */
symbol: string;
decimals: 18;
};
/**
* @public
*/
export const NATIVE_TOKEN_ADDRESS =
"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
/**
* @public
*/
export const NATIVE_TOKENS: Record<SUPPORTED_CHAIN_ID, NativeCurrency> = {
[ChainId.Mainnet]: {
name: "Ether",
symbol: "ETH",
decimals: 18,
},
[ChainId.Goerli]: {
name: "Görli Ether",
symbol: "GOR",
decimals: 18,
},
[ChainId.Polygon]: {
name: "Matic",
symbol: "MATIC",
decimals: 18,
},
[ChainId.Mumbai]: {
name: "Matic",
symbol: "MATIC",
decimals: 18,
},
[ChainId.Avalanche]: {
name: "Avalanche",
symbol: "AVAX",
decimals: 18,
},
[ChainId.AvalancheFujiTestnet]: {
name: "Avalanche",
symbol: "AVAX",
decimals: 18,
},
[ChainId.Fantom]: {
name: "Fantom",
symbol: "FTM",
decimals: 18,
},
[ChainId.FantomTestnet]: {
name: "Fantom",
symbol: "FTM",
decimals: 18,
},
[ChainId.Arbitrum]: {
name: "Ether",
symbol: "ETH",
decimals: 18,
},
[ChainId.ArbitrumGoerli]: {
name: "Arbitrum Goerli Ether",
symbol: "AGOR",
decimals: 18,
},
[ChainId.Optimism]: {
name: "Ether",
symbol: "ETH",
decimals: 18,
},
[ChainId.OptimismGoerli]: {
name: "Goerli Ether",
symbol: "ETH",
decimals: 18,
},
[ChainId.BinanceSmartChainMainnet]: {
name: "Binance Chain Native Token",
symbol: "BNB",
decimals: 18,
},
[ChainId.BinanceSmartChainTestnet]: {
name: "Binance Chain Native Token",
symbol: "TBNB",
decimals: 18,
},
};
export const DEFAULT_API_KEY =
"c6634ad2d97b74baf15ff556016830c251050e6c36b9da508ce3ec80095d3dc1";
export function getRpcUrl(network: string) {
return `https://${network}.rpc.thirdweb.com/${DEFAULT_API_KEY}`;
}
/**
* Returns the native token for a given chain
* @param chainId - the chain id
* @public
*/
export function getNativeTokenByChainId(chainId: ChainId): NativeCurrency {
return NATIVE_TOKENS[chainId as SUPPORTED_CHAIN_ID];
}
const supportedChains: Record<SUPPORTED_CHAIN_ID, Chain> = {
[ChainId.Mainnet]: {
id: ChainId.Mainnet,
name: "Ethereum",
network: "homestead",
nativeCurrency: getNativeTokenByChainId(ChainId.Mainnet),
rpcUrls: {
alchemy: alchemyRpcUrls.mainnet,
default: getRpcUrl("ethereum"),
infura: infuraRpcUrls.mainnet,
public: getRpcUrl("ethereum"),
},
blockExplorers: {
etherscan: etherscanBlockExplorers.mainnet,
default: etherscanBlockExplorers.mainnet,
},
},
[ChainId.Goerli]: {
id: ChainId.Goerli,
name: "Goerli",
network: "goerli",
nativeCurrency: getNativeTokenByChainId(ChainId.Goerli),
rpcUrls: {
alchemy: alchemyRpcUrls.goerli,
default: getRpcUrl("goerli"),
infura: infuraRpcUrls.goerli,
public: getRpcUrl("goerli"),
},
blockExplorers: {
etherscan: etherscanBlockExplorers.goerli,
default: etherscanBlockExplorers.goerli,
},
testnet: true,
},
[ChainId.Polygon]: {
id: ChainId.Polygon,
name: "Polygon",
network: "matic",
nativeCurrency: getNativeTokenByChainId(ChainId.Polygon),
rpcUrls: {
alchemy: alchemyRpcUrls.polygon,
default: getRpcUrl("polygon"),
infura: infuraRpcUrls.polygon,
public: getRpcUrl("polygon"),
},
blockExplorers: {
etherscan: etherscanBlockExplorers.polygon,
default: etherscanBlockExplorers.polygon,
},
},
[ChainId.Mumbai]: {
id: ChainId.Mumbai,
name: "Mumbai",
network: "maticmum",
nativeCurrency: getNativeTokenByChainId(ChainId.Mumbai),
rpcUrls: {
alchemy: alchemyRpcUrls.polygonMumbai,
default: getRpcUrl("mumbai"),
infura: infuraRpcUrls.polygonMumbai,
public: getRpcUrl("mumbai"),
},
blockExplorers: {
etherscan: etherscanBlockExplorers.polygonMumbai,
default: etherscanBlockExplorers.polygonMumbai,
},
testnet: true,
},
[ChainId.Avalanche]: {
id: ChainId.Avalanche,
name: "Avalanche",
network: "avalanche",
nativeCurrency: getNativeTokenByChainId(ChainId.Avalanche),
rpcUrls: {
default: getRpcUrl("avalanche"),
public: getRpcUrl("avalanche"),
},
blockExplorers: {
default: {
name: "SnowTrace",
url: "https://snowtrace.io/",
},
},
},
[ChainId.AvalancheFujiTestnet]: {
id: ChainId.AvalancheFujiTestnet,
name: "Avalanche Fuji Testnet",
network: "avalanche-fuji",
nativeCurrency: getNativeTokenByChainId(ChainId.AvalancheFujiTestnet),
rpcUrls: {
default: getRpcUrl("avalanche-fuji"),
public: getRpcUrl("avalanche-fuji"),
},
blockExplorers: {
default: {
name: "SnowTrace",
url: "https://testnet.snowtrace.io/",
},
},
testnet: true,
},
[ChainId.Fantom]: {
id: ChainId.Fantom,
name: "Fantom Opera",
network: "fantom",
nativeCurrency: getNativeTokenByChainId(ChainId.Fantom),
rpcUrls: {
default: getRpcUrl("fantom"),
public: getRpcUrl("fantom"),
},
blockExplorers: {
default: {
name: "Fantom Explorer",
url: "https://ftmscan.com/",
},
},
},
[ChainId.FantomTestnet]: {
id: ChainId.FantomTestnet,
name: "Fantom Opera Testnet",
network: "fantom-testnet",
nativeCurrency: getNativeTokenByChainId(ChainId.FantomTestnet),
rpcUrls: {
default: getRpcUrl("fantom-testnet"),
public: getRpcUrl("fantom-testnet"),
},
blockExplorers: {
default: {
name: "Fantom Explorer",
url: "https://testnet.ftmscan.com/",
},
},
testnet: true,
},
[ChainId.Optimism]: {
id: ChainId.Optimism,
name: "Optimism",
network: "optimism",
nativeCurrency: getNativeTokenByChainId(ChainId.Optimism),
rpcUrls: {
alchemy: alchemyRpcUrls.optimism,
default: getRpcUrl("optimism"),
infura: infuraRpcUrls.optimism,
public: getRpcUrl("optimism"),
},
blockExplorers: {
etherscan: etherscanBlockExplorers.optimism,
default: etherscanBlockExplorers.optimism,
},
},
[ChainId.OptimismGoerli]: {
id: ChainId.OptimismGoerli,
name: "Optimism Goerli",
network: "optimism-goerli",
nativeCurrency: getNativeTokenByChainId(ChainId.OptimismGoerli),
rpcUrls: {
alchemy: alchemyRpcUrls.optimismGoerli,
default: getRpcUrl("optimism-goerli"),
infura: infuraRpcUrls.optimismGoerli,
public: getRpcUrl("optimism-goerli"),
},
blockExplorers: {
etherscan: etherscanBlockExplorers.optimismGoerli,
default: etherscanBlockExplorers.optimismGoerli,
},
testnet: true,
},
[ChainId.Arbitrum]: {
id: ChainId.Arbitrum,
name: "Arbitrum One",
network: "arbitrum",
nativeCurrency: getNativeTokenByChainId(ChainId.Arbitrum),
rpcUrls: {
alchemy: alchemyRpcUrls.arbitrum,
default: getRpcUrl("arbitrum"),
infura: infuraRpcUrls.arbitrum,
public: getRpcUrl("arbitrum"),
},
blockExplorers: {
etherscan: etherscanBlockExplorers.arbitrum,
default: etherscanBlockExplorers.arbitrum,
},
},
[ChainId.ArbitrumGoerli]: {
id: ChainId.ArbitrumGoerli,
name: "Arbitrum Goerli",
network: "arbitrum-goerli",
nativeCurrency: getNativeTokenByChainId(ChainId.ArbitrumGoerli),
rpcUrls: {
alchemy: alchemyRpcUrls.arbitrumGoerli,
default: getRpcUrl("arbitrum-goerli"),
infura: infuraRpcUrls.arbitrumGoerli,
public: getRpcUrl("arbitrum-goerli"),
},
blockExplorers: {
etherscan: etherscanBlockExplorers.arbitrumGoerli,
default: etherscanBlockExplorers.arbitrumGoerli,
},
testnet: true,
},
[ChainId.BinanceSmartChainMainnet]: {
id: ChainId.BinanceSmartChainMainnet,
name: "Binance Smart Chain",
network: "bsc",
nativeCurrency: getNativeTokenByChainId(ChainId.BinanceSmartChainMainnet),
rpcUrls: {
default: getRpcUrl("binance"),
public: getRpcUrl("binance"),
},
blockExplorers: {
default: {
name: "BscScan",
url: "https://bscscan.com/",
},
},
},
[ChainId.BinanceSmartChainTestnet]: {
id: ChainId.BinanceSmartChainTestnet,
name: "Binance Smart Chain Testnet",
network: "bsc-testnet",
nativeCurrency: getNativeTokenByChainId(ChainId.BinanceSmartChainTestnet),
rpcUrls: {
default: getRpcUrl("binance-testnet"),
public: getRpcUrl("binance-testnet"),
},
blockExplorers: {
default: {
name: "BscScan",
url: "https://testnet.bscscan.com/",
},
},
testnet: true,
},
};
export const thirdwebChains: Chain[] = Object.values(supportedChains);
+11
View File
@@ -0,0 +1,11 @@
export * from "./wallets/coinbase-wallet";
export * from "./wallets/metamask";
export * from "./wallets/wallet-connect";
export * from "./wallets/injected";
// just the types
export type {
AbstractWallet,
WalletData,
WalletEvents,
WalletOptions,
} from "./wallets/base";
@@ -0,0 +1,81 @@
import { Ethereum } from "@wagmi/core";
export function getInjectedName(ethereum?: Ethereum) {
if (!ethereum) {
return "Injected";
}
const getName = (provider: Ethereum) => {
if (provider.isAvalanche) {
return "Core Wallet";
}
if (provider.isBitKeep) {
return "BitKeep";
}
if (provider.isBraveWallet) {
return "Brave Wallet";
}
if (provider.isCoinbaseWallet) {
return "Coinbase Wallet";
}
if (provider.isExodus) {
return "Exodus";
}
if (provider.isFrame) {
return "Frame";
}
if (provider.isKuCoinWallet) {
return "KuCoin Wallet";
}
if (provider.isMathWallet) {
return "MathWallet";
}
if (provider.isOneInchIOSWallet || provider.isOneInchAndroidWallet) {
return "1inch Wallet";
}
if (provider.isOpera) {
return "Opera";
}
if (provider.isPortal) {
return "Ripio Portal";
}
if (provider.isTally) {
return "Tally";
}
if (provider.isTokenPocket) {
return "TokenPocket";
}
if (provider.isTokenary) {
return "Tokenary";
}
if (provider.isTrust || provider.isTrustWallet) {
return "Trust Wallet";
}
if (provider.isMetaMask) {
return "MetaMask";
}
};
// Some injected providers detect multiple other providers and create a list at `ethers.providers`
if (ethereum.providers?.length) {
// Deduplicate names using Set
// Coinbase Wallet puts multiple providers in `ethereum.providers`
const nameSet = new Set<string>();
let unknownCount = 1;
for (const provider of ethereum.providers) {
let name = getName(provider);
if (!name) {
name = `Unknown Wallet #${unknownCount}`;
unknownCount += 1;
}
nameSet.add(name);
}
const names = [...nameSet];
if (names.length) {
return names;
}
return names[0] ?? "Injected";
}
return getName(ethereum) ?? "Injected";
}
+46
View File
@@ -0,0 +1,46 @@
import localforage from "localforage";
function prefixKey(key: string) {
return `tw_${key}`;
}
let connectorStorage: LocalForage;
export function getConnectorStorage() {
if (!connectorStorage) {
connectorStorage = localforage.createInstance({
name: prefixKey("connectors"),
storeName: prefixKey("connectors"),
version: 1.0,
});
}
return connectorStorage;
}
let coordinatorStorage: LocalForage;
export function getCoordinatorStorage() {
if (!coordinatorStorage) {
coordinatorStorage = localforage.createInstance({
name: prefixKey("coordinator"),
storeName: prefixKey("coordinator"),
version: 1.0,
});
}
return coordinatorStorage;
}
const walletStorage: Map<string, LocalForage> = new Map();
export function getWalletStorage(walletId: string) {
let storage = walletStorage.get(walletId);
if (!storage) {
storage = localforage.createInstance({
name: prefixKey(`wallet:${walletId}`),
storeName: prefixKey(`wallet_${walletId}`),
version: 1.0,
});
walletStorage.set(walletId, storage);
}
return storage;
}
+143
View File
@@ -0,0 +1,143 @@
import { thirdwebChains } from "../constants/chains";
import { getCoordinatorStorage, getWalletStorage } from "../utils/storage";
import type { Chain, Connector as WagmiConnector } from "@wagmi/core";
import EventEmitter from "eventemitter3";
export type WalletData = {
address?: string;
chainId?: number;
};
export interface WalletEvents {
connect(data: WalletData): void;
change(data: WalletData): void;
message({ type, data }: { type: string; data?: unknown }): void;
disconnect(): void;
error(error: Error): void;
}
export type WalletOptions = {
chains?: Chain[];
// default: true
shouldAutoConnect?: boolean;
appName: string;
};
export abstract class AbstractWallet extends EventEmitter<WalletEvents> {
#wallletId;
protected coordinatorStorage;
protected walletStorage;
protected chains;
protected options: WalletOptions;
constructor(walletId: string, options: WalletOptions) {
super();
this.#wallletId = walletId;
this.options = options;
this.chains = options.chains || thirdwebChains;
this.coordinatorStorage = getCoordinatorStorage();
this.walletStorage = getWalletStorage(walletId);
if (options.shouldAutoConnect !== false) {
this.autoConnect();
}
}
async autoConnect() {
const lastConnectedWallet = await this.coordinatorStorage.getItem(
"lastConnectedWallet",
);
if (lastConnectedWallet === this.#wallletId) {
const lastConnectedChain = await this.walletStorage.getItem(
"lastConnectedChain",
);
let parsedChain: number | undefined;
try {
parsedChain = parseInt(lastConnectedChain as string);
if (isNaN(parsedChain)) {
parsedChain = undefined;
}
} catch {
parsedChain = undefined;
}
const connector = await this.getConnector();
if (await connector.isAuthorized()) {
return await this.connect(parsedChain);
}
}
}
protected abstract getConnector(): Promise<WagmiConnector>;
async connect(
chainId?: number,
): Promise<{ address: string; chainId: number }> {
const connector = await this.getConnector();
// setup listeners to re-expose events
connector.on("connect", (data) => {
this.coordinatorStorage.setItem("lastConnectedWallet", this.#wallletId);
this.emit("connect", { address: data.account, chainId: data.chain?.id });
if (data.chain?.id) {
this.walletStorage.setItem("lastConnectedChain", data.chain?.id);
}
});
connector.on("change", (data) => {
this.emit("change", { address: data.account, chainId: data.chain?.id });
if (data.chain?.id) {
this.walletStorage.setItem("lastConnectedChain", data.chain?.id);
}
});
connector.on("message", (data) => this.emit("message", data));
connector.on("disconnect", () => this.emit("disconnect"));
connector.on("error", (error) => this.emit("error", error));
// end event listener setups
let connectionRes = await connector.connect({ chainId });
// do not break on coordinator error
try {
await this.coordinatorStorage.setItem(
"lastConnectedWallet",
this.#wallletId,
);
} catch {}
return {
address: connectionRes.account,
chainId: connectionRes.chain?.id,
};
}
async getSigner(chainId?: number) {
const connector = await this.getConnector();
if (!connector) {
throw new Error("Wallet not connected");
}
return await connector.getSigner({ chainId });
}
public async disconnect() {
const connector = await this.getConnector();
if (connector) {
connector.removeAllListeners();
await connector.disconnect();
// get the last connected wallet and check if it's this wallet, if so, remove it
const lastConnectedWallet = await this.coordinatorStorage.getItem(
"lastConnectedWallet",
);
if (lastConnectedWallet === this.#wallletId) {
await this.coordinatorStorage.removeItem("lastConnectedWallet");
}
}
}
async switchChain(chainId: number): Promise<Chain> {
const connector = await this.getConnector();
if (!connector) {
throw new Error("Wallet not connected");
}
if (!connector.switchChain) {
throw new Error("Wallet does not support switching chains");
}
return await connector.switchChain(chainId);
}
}
@@ -0,0 +1,32 @@
import type { CoinbaseWalletConnector } from "../connectors/coinbase-wallet";
import { AbstractWallet, WalletOptions } from "./base";
export class CoinbaseWallet extends AbstractWallet {
#connector?: CoinbaseWalletConnector;
static id = "coinbaseWallet" as const;
public get walletName() {
return "Coinbase Wallet" as const;
}
constructor(options: WalletOptions) {
super(CoinbaseWallet.id, options);
}
protected async getConnector(): Promise<CoinbaseWalletConnector> {
if (!this.#connector) {
// import the connector dynamically
const { CoinbaseWalletConnector } = await import(
"../connectors/coinbase-wallet"
);
this.#connector = new CoinbaseWalletConnector({
chains: this.chains,
options: {
appName: this.options.appName,
reloadOnDisconnect: false,
},
});
}
return this.#connector;
}
}
+29
View File
@@ -0,0 +1,29 @@
import type { InjectedConnector } from "../connectors/injected";
import { AbstractWallet, WalletOptions } from "./base";
export class InjectedWallet extends AbstractWallet {
#connector?: InjectedConnector;
static id = "injected" as const;
public get walletName() {
return this.#connector?.name || "Injected Wallet";
}
constructor(options: WalletOptions) {
super(InjectedWallet.id, options);
}
protected async getConnector(): Promise<InjectedConnector> {
if (!this.#connector) {
// import the connector dynamically
const { InjectedConnector } = await import("../connectors/injected");
this.#connector = new InjectedConnector({
chains: this.chains,
options: {
shimDisconnect: true,
},
});
}
return this.#connector;
}
}
+30
View File
@@ -0,0 +1,30 @@
import type { MetaMaskConnector } from "../connectors/metamask";
import { AbstractWallet, WalletOptions } from "./base";
export class MetaMask extends AbstractWallet {
#connector?: MetaMaskConnector;
static id = "metamask" as const;
public get walletName() {
return "MetaMask" as const;
}
constructor(options: WalletOptions) {
super(MetaMask.id, options);
}
protected async getConnector(): Promise<MetaMaskConnector> {
if (!this.#connector) {
// import the connector dynamically
const { MetaMaskConnector } = await import("../connectors/metamask");
this.#connector = new MetaMaskConnector({
chains: this.chains,
options: {
shimDisconnect: true,
},
});
}
return this.#connector;
}
}
@@ -0,0 +1,30 @@
import type { WalletConnectConnector } from "../connectors/wallet-connect";
import { AbstractWallet, WalletOptions } from "./base";
export class WalletConnect extends AbstractWallet {
#connector?: WalletConnectConnector;
static id = "walletConnect" as const;
public get walletName() {
return this.#connector?.name || ("WalletConnect" as const);
}
constructor(options: WalletOptions) {
super(WalletConnect.id, options);
}
protected async getConnector(): Promise<WalletConnectConnector> {
if (!this.#connector) {
// import the connector dynamically
const { WalletConnectConnector } = await import(
"../connectors/wallet-connect"
);
this.#connector = new WalletConnectConnector({
chains: this.chains,
options: {},
});
}
return this.#connector;
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "@thirdweb-dev/tsconfig/sdk.json",
"include": ["src", "types"],
"exclude": ["dist", "build", "node_modules"]
}

Some files were not shown because too many files have changed in this diff Show More