Compare commits
54
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25ef4be50f | ||
|
|
ab1f8bb6f6 | ||
|
|
5f8ec45af7 | ||
|
|
b45d761f8f | ||
|
|
04de5b2783 | ||
|
|
dee4596549 | ||
|
|
3547901ddd | ||
|
|
fe7ab1c5d3 | ||
|
|
bdd9ed2298 | ||
|
|
0977454d45 | ||
|
|
19b8e18b6b | ||
|
|
7c0060b7fd | ||
|
|
0a07d7a248 | ||
|
|
8c81ca5c30 | ||
|
|
c911d183e6 | ||
|
|
f15733f34c | ||
|
|
a6c074c3f3 | ||
|
|
fdb9baedee | ||
|
|
9e591b32b7 | ||
|
|
12ec3be4a3 | ||
|
|
48893c730e | ||
|
|
f37e86b7d2 | ||
|
|
62b7388bb2 | ||
|
|
065c4db41b | ||
|
|
431ab23ee2 | ||
|
|
836484ba98 | ||
|
|
5047686f2a | ||
|
|
7cab8d1679 | ||
|
|
0f0125acf2 | ||
|
|
894cbefc63 | ||
|
|
73dc026ff9 | ||
|
|
af32007b9f | ||
|
|
5fba324ac1 | ||
|
|
bff433e121 | ||
|
|
68c8e3c4d7 | ||
|
|
4e8ff58208 | ||
|
|
d0d5a3c23d | ||
|
|
9eaa21d09a | ||
|
|
557429b5cf | ||
|
|
96d2dfd25d | ||
|
|
5862c558fb | ||
|
|
e913e0daa9 | ||
|
|
f7a74a3b7c | ||
|
|
73883f56d0 | ||
|
|
e47085af92 | ||
|
|
f525e2cc23 | ||
|
|
af85389dc0 | ||
|
|
ccb4ce8d00 | ||
|
|
4cdd0bd634 | ||
|
|
ea95c5f609 | ||
|
|
699a2b16fb | ||
|
|
a8267f912d | ||
|
|
c673e39f23 | ||
|
|
08d04fe4dc |
@@ -2,7 +2,9 @@
|
||||
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
|
||||
"changelog": ["@changesets/changelog-github", { "repo": "thirdweb-dev/js" }],
|
||||
"commit": false,
|
||||
"fixed": [["@thirdweb-dev/sdk", "@thirdweb-dev/react"]],
|
||||
"fixed": [
|
||||
["@thirdweb-dev/sdk", "@thirdweb-dev/react-core", "@thirdweb-dev/react"]
|
||||
],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
|
||||
@@ -13,7 +13,7 @@ concurrency:
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Test
|
||||
# timeout-minutes: 15
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-32
|
||||
# To use Remote Caching, uncomment the next lines and follow the steps below.
|
||||
env:
|
||||
|
||||
@@ -12,7 +12,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
integration-tests:
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-32
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -13,8 +13,7 @@ concurrency:
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
# timeout-minutes: 15
|
||||
# try out buildjet to speed up build and test
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
# To use Remote Caching, uncomment the next lines and follow the steps below.
|
||||
env:
|
||||
|
||||
@@ -41,6 +41,7 @@ jobs:
|
||||
|
||||
deploy:
|
||||
runs-on: ubuntu-latest-32
|
||||
timeout-minutes: 30
|
||||
needs: deploy-check
|
||||
if: needs.deploy-check.outputs.triggered == 'true'
|
||||
steps:
|
||||
|
||||
@@ -14,6 +14,7 @@ concurrency:
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-32
|
||||
steps:
|
||||
- name: Checkout branch
|
||||
|
||||
@@ -5,6 +5,7 @@ on: [pull_request]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest-32
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
+2
-1
@@ -36,8 +36,9 @@
|
||||
"@changesets/cli": "^2.24.3",
|
||||
"@manypkg/cli": "^0.19.1",
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
"@trivago/prettier-plugin-sort-imports": "^3.3.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.0.0",
|
||||
"eslint": "^8.21.0",
|
||||
"mocha": "^10.2.0",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.7.1",
|
||||
"turbo": "^1.4.6",
|
||||
|
||||
@@ -1,5 +1,58 @@
|
||||
# @thirdweb-dev/auth
|
||||
|
||||
## 3.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#533](https://github.com/thirdweb-dev/js/pull/533) [`dee4596`](https://github.com/thirdweb-dev/js/commit/dee45965496d5d0298944031dd13a4345f9e1683) Thanks [@adam-maj](https://github.com/adam-maj)! - Remove next major version pin on auth
|
||||
|
||||
## 3.0.2
|
||||
|
||||
## 3.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#520](https://github.com/thirdweb-dev/js/pull/520) [`8c81ca5`](https://github.com/thirdweb-dev/js/commit/8c81ca5c3033b04b1f64e3a1134a72e7e3ec03b1) Thanks [@adam-maj](https://github.com/adam-maj)! - Update auth and react-core dependencies
|
||||
|
||||
## 3.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- [#460](https://github.com/thirdweb-dev/js/pull/460) [`a6c074c`](https://github.com/thirdweb-dev/js/commit/a6c074c3f33148cd17f5a66a58df9272a4381bab) Thanks [@adam-maj](https://github.com/adam-maj)! - Complete Auth redesign and update to add a number of major and quality of life improvements, including the following:
|
||||
|
||||
- Ability to use Auth APIs with both cookies and JWTs, allowing non browser clients to interact with Auth (mobile, gaming, scripts, etc.)
|
||||
- Ability to store session data and other data on the Auth user
|
||||
- Callbacks to run side-effects on login, logout, and requesting user data
|
||||
- Ability to configure cookies for custom domains and backend setups
|
||||
- Support for validation of the entire EIP4361/CAIP122 specification
|
||||
- No more need for redirects or payload encoding on Auth requests
|
||||
- and more...
|
||||
|
||||
See the new documentation to view the new changes and usage: [Auth Documentation](https://portal.thirdweb.com/auth).
|
||||
|
||||
## How to upgrade
|
||||
|
||||
The `ThirdwebAuth` constructor now takes the `domain` in the constructor, and takes a more generic `wallet` interface as input. The `wallet` can be imported from the `@thirdweb-dev/wallets` package, or for more simpler use cases, from the `@thirdweb-dev/auth/evm` and `@thirdweb-dev/auth/solana` entrypoints.
|
||||
|
||||
```js
|
||||
import { PrivateKeyWallet } from "@thirdweb-dev/auth/evm";
|
||||
|
||||
// Pass in domain and wallet to the constructor
|
||||
const wallet = new PrivateKeyWallet("0x...");
|
||||
const auth = new ThirdwebAuth(wallet, "example.com");
|
||||
|
||||
// Auth functions no longer require domain to be passed in
|
||||
const payload = await auth.login();
|
||||
```
|
||||
|
||||
## 2.0.41
|
||||
|
||||
## 2.0.40
|
||||
|
||||
## 2.0.39
|
||||
|
||||
## 2.0.38
|
||||
|
||||
## 2.0.37
|
||||
|
||||
## 2.0.36
|
||||
|
||||
@@ -16,9 +16,13 @@
|
||||
Install the latest version of the SDK with either `npm` or `yarn`:
|
||||
|
||||
```shell
|
||||
npm install @thirdweb-dev/auth @thirdweb-dev/sdk ethers
|
||||
npm install @thirdweb-dev/auth
|
||||
```
|
||||
|
||||
```shell
|
||||
yarn add @thirdweb-dev/auth @thirdweb-dev/sdk ethers
|
||||
yarn add @thirdweb-dev/auth
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Auth Docs](https://portal.thirdweb.com/auth)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"main": "dist/thirdweb-dev-auth-evm.cjs.js",
|
||||
"module": "dist/thirdweb-dev-auth-evm.esm.js"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"main": "dist/thirdweb-dev-auth-next-evm.cjs.js",
|
||||
"module": "dist/thirdweb-dev-auth-next-evm.esm.js"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"main": "dist/thirdweb-dev-auth-next-solana.cjs.js",
|
||||
"module": "dist/thirdweb-dev-auth-next-solana.esm.js"
|
||||
}
|
||||
+62
-28
@@ -1,52 +1,54 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/auth",
|
||||
"version": "2.0.37",
|
||||
"version": "3.0.3",
|
||||
"main": "dist/thirdweb-dev-auth.cjs.js",
|
||||
"module": "dist/thirdweb-dev-auth.esm.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"module": "./dist/thirdweb-dev-auth.esm.js",
|
||||
"default": "./dist/thirdweb-dev-auth.cjs.js"
|
||||
},
|
||||
"./evm": {
|
||||
"module": "./evm/dist/thirdweb-dev-auth-evm.esm.js",
|
||||
"default": "./evm/dist/thirdweb-dev-auth-evm.cjs.js"
|
||||
},
|
||||
"./next": {
|
||||
"module": "./next/dist/thirdweb-dev-auth-next.esm.js",
|
||||
"default": "./next/dist/thirdweb-dev-auth-next.cjs.js"
|
||||
},
|
||||
"./solana": {
|
||||
"module": "./solana/dist/thirdweb-dev-auth-solana.esm.js",
|
||||
"default": "./solana/dist/thirdweb-dev-auth-solana.cjs.js"
|
||||
},
|
||||
"./express": {
|
||||
"module": "./express/dist/thirdweb-dev-auth-express.esm.js",
|
||||
"default": "./express/dist/thirdweb-dev-auth-express.cjs.js"
|
||||
},
|
||||
"./next/evm": {
|
||||
"module": "./next/evm/dist/thirdweb-dev-auth-next-evm.esm.js",
|
||||
"default": "./next/evm/dist/thirdweb-dev-auth-next-evm.cjs.js"
|
||||
},
|
||||
"./next-auth": {
|
||||
"module": "./next-auth/dist/thirdweb-dev-auth-next-auth.esm.js",
|
||||
"default": "./next-auth/dist/thirdweb-dev-auth-next-auth.cjs.js"
|
||||
},
|
||||
"./next/solana": {
|
||||
"module": "./next/solana/dist/thirdweb-dev-auth-next-solana.esm.js",
|
||||
"default": "./next/solana/dist/thirdweb-dev-auth-next-solana.cjs.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/auth",
|
||||
"author": "thirdweb eng <[email protected]>",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"next/**/*",
|
||||
"express/**/*",
|
||||
"next-auth/**/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "preconstruct build",
|
||||
"format": "prettier --write 'src/**/*'",
|
||||
"lint": "eslint src/",
|
||||
"fix": "eslint src/ --fix",
|
||||
"clean": "rm -rf dist/ && rm -rf node_modules/"
|
||||
"clean": "rm -rf dist/ && rm -rf node_modules/",
|
||||
"test": "mocha --config './test/.mocharc.json' --timeout 30000 --parallel './test/**/*.test.ts'"
|
||||
},
|
||||
"preconstruct": {
|
||||
"entrypoints": [
|
||||
"next/index.ts",
|
||||
"next/evm/index.ts",
|
||||
"next/solana/index.ts",
|
||||
"index.ts",
|
||||
"express/index.ts",
|
||||
"next-auth/index.ts"
|
||||
"next/index.ts",
|
||||
"next-auth/index.ts",
|
||||
"evm/index.ts",
|
||||
"solana/index.ts"
|
||||
],
|
||||
"___experimentalFlags_WILL_CHANGE_IN_PATCH": {
|
||||
"exports": true
|
||||
@@ -61,12 +63,20 @@
|
||||
"@microsoft/api-extractor": "^7.29.2",
|
||||
"@microsoft/tsdoc": "^0.14.1",
|
||||
"@preconstruct/cli": "^2.2.1",
|
||||
"@thirdweb-dev/sdk": "*",
|
||||
"@solana/web3.js": "^1.73.0",
|
||||
"@swc-node/register": "^1.5.4",
|
||||
"@swc/core": "^1.3.23",
|
||||
"@thirdweb-dev/wallets": "*",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/chai": "^4.3.4",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.33.0",
|
||||
"@typescript-eslint/parser": "^5.33.0",
|
||||
"@wagmi/core": "^0.8.18",
|
||||
"chai": "^4.3.7",
|
||||
"eslint": "^8.21.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
@@ -75,6 +85,7 @@
|
||||
"eslint-plugin-tsdoc": "^0.2.16",
|
||||
"ethers": "^5.7.2",
|
||||
"express": "^4.18.1",
|
||||
"mocha": "^10.2.0",
|
||||
"next": "^12.2.0",
|
||||
"next-auth": "^4.10.3",
|
||||
"prettier": "^2.7.1",
|
||||
@@ -83,13 +94,35 @@
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@thirdweb-dev/sdk": "*",
|
||||
"ethers": ">=5.5.1",
|
||||
"express": "^4.18.1",
|
||||
"next": "^12.2.0",
|
||||
"next-auth": "^4.10.3"
|
||||
"@noble/ed25519": "^1.7.1",
|
||||
"@solana/web3.js": "^1.73.0",
|
||||
"bs58": "^5.0.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"ethers": "^5",
|
||||
"express": "^4",
|
||||
"next": "^12 || ^13",
|
||||
"next-auth": "^4",
|
||||
"tweetnacl": "^1.0.3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@noble/ed25519": {
|
||||
"optional": true
|
||||
},
|
||||
"@solana/web3.js": {
|
||||
"optional": true
|
||||
},
|
||||
"bs58": {
|
||||
"optional": true
|
||||
},
|
||||
"cookie-parser": {
|
||||
"optional": true
|
||||
},
|
||||
"tweetnacl": {
|
||||
"optional": true
|
||||
},
|
||||
"ethers": {
|
||||
"optional": true
|
||||
},
|
||||
"express": {
|
||||
"optional": true
|
||||
},
|
||||
@@ -102,6 +135,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie": "^0.5.0",
|
||||
"cookie-parser": "^1.4.6"
|
||||
"uuid": "^9.0.0",
|
||||
"zod": "^3.20.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"main": "dist/thirdweb-dev-auth-solana.cjs.js",
|
||||
"module": "dist/thirdweb-dev-auth-solana.esm.js"
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
import {
|
||||
LoginOptions,
|
||||
LoginPayload,
|
||||
GenerateOptions,
|
||||
LoginPayloadData,
|
||||
LoginPayloadDataSchema,
|
||||
AuthenticationPayloadDataSchema,
|
||||
AuthenticationPayloadData,
|
||||
LoginOptionsSchema,
|
||||
VerifyOptionsSchema,
|
||||
VerifyOptions,
|
||||
GenerateOptionsSchema,
|
||||
AuthenticateOptionsSchema,
|
||||
AuthenticateOptions,
|
||||
User,
|
||||
Json,
|
||||
} from "./schema";
|
||||
import { isBrowser } from "./utils";
|
||||
import type { GenericAuthWallet } from "@thirdweb-dev/wallets";
|
||||
|
||||
export class ThirdwebAuth {
|
||||
private domain: string;
|
||||
private wallet: GenericAuthWallet;
|
||||
|
||||
constructor(wallet: GenericAuthWallet, domain: string) {
|
||||
this.wallet = wallet;
|
||||
this.domain = domain;
|
||||
}
|
||||
|
||||
public updateWallet(wallet: GenericAuthWallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public async login(options?: LoginOptions): Promise<LoginPayload> {
|
||||
const parsedOptions = LoginOptionsSchema.parse(options);
|
||||
|
||||
let chainId: string | undefined = parsedOptions?.chainId;
|
||||
if (!chainId && this.wallet.getChainId) {
|
||||
try {
|
||||
chainId = (await this.wallet.getChainId()).toString();
|
||||
} catch {
|
||||
// ignore error
|
||||
}
|
||||
}
|
||||
|
||||
const payloadData = LoginPayloadDataSchema.parse({
|
||||
type: this.wallet.type,
|
||||
domain: parsedOptions?.domain || this.domain,
|
||||
address: await this.wallet.getAddress(),
|
||||
statement: parsedOptions?.statement,
|
||||
version: parsedOptions?.version,
|
||||
uri:
|
||||
parsedOptions?.uri || (isBrowser() ? window.location.href : undefined),
|
||||
chain_id: chainId,
|
||||
nonce: parsedOptions?.nonce,
|
||||
expiration_time:
|
||||
parsedOptions?.expirationTime || new Date(Date.now() + 1000 * 60 * 5),
|
||||
invalid_before: parsedOptions?.invalidBefore,
|
||||
resources: parsedOptions?.resources,
|
||||
});
|
||||
|
||||
const message = this.generateMessage(payloadData);
|
||||
const signature = await this.wallet.signMessage(message);
|
||||
|
||||
return {
|
||||
payload: payloadData,
|
||||
signature,
|
||||
};
|
||||
}
|
||||
|
||||
public async verify(
|
||||
payload: LoginPayload,
|
||||
options?: VerifyOptions,
|
||||
): Promise<string> {
|
||||
const parsedOptions = VerifyOptionsSchema.parse(options);
|
||||
|
||||
if (payload.payload.type !== this.wallet.type) {
|
||||
throw new Error(
|
||||
`Expected chain type '${this.wallet.type}' does not match chain type on payload '${payload.payload.type}'`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check that the intended domain matches the domain of the payload
|
||||
const domain = parsedOptions?.domain || this.domain;
|
||||
if (payload.payload.domain !== domain) {
|
||||
throw new Error(
|
||||
`Expected domain '${domain}' does not match domain on payload '${payload.payload.domain}'`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check that the payload statement matches the expected statement
|
||||
if (parsedOptions?.statement) {
|
||||
if (payload.payload.statement !== parsedOptions.statement) {
|
||||
throw new Error(
|
||||
`Expected statement '${parsedOptions.statement}' does not match statement on payload '${payload.payload.statement}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the intended URI matches the URI of the payload
|
||||
if (parsedOptions?.uri) {
|
||||
if (payload.payload.uri !== parsedOptions.uri) {
|
||||
throw new Error(
|
||||
`Expected URI '${parsedOptions.uri}' does not match URI on payload '${payload.payload.uri}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the intended version matches the version of the payload
|
||||
if (parsedOptions?.version) {
|
||||
if (payload.payload.version !== parsedOptions.version) {
|
||||
throw new Error(
|
||||
`Expected version '${parsedOptions.version}' does not match version on payload '${payload.payload.version}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the intended chain ID matches the chain ID of the payload
|
||||
if (parsedOptions?.chainId) {
|
||||
if (payload.payload.chain_id !== parsedOptions.chainId) {
|
||||
throw new Error(
|
||||
`Expected chain ID '${parsedOptions.chainId}' does not match chain ID on payload '${payload.payload.chain_id}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the payload nonce is valid
|
||||
if (parsedOptions?.validateNonce !== undefined) {
|
||||
try {
|
||||
await parsedOptions.validateNonce(payload.payload.nonce);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw new Error(`Login request nonce is invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that it isn't before the invalid before time
|
||||
const currentTime = new Date();
|
||||
if (currentTime < new Date(payload.payload.invalid_before)) {
|
||||
throw new Error(`Login request is not yet valid`);
|
||||
}
|
||||
|
||||
// Check that the payload hasn't expired
|
||||
if (currentTime > new Date(payload.payload.expiration_time)) {
|
||||
throw new Error(`Login request has expired`);
|
||||
}
|
||||
|
||||
// Check that the specified resources are present on the payload
|
||||
if (parsedOptions?.resources) {
|
||||
const missingResources = parsedOptions.resources.filter(
|
||||
(resource) => !payload.payload.resources?.includes(resource),
|
||||
);
|
||||
if (missingResources.length > 0) {
|
||||
throw new Error(
|
||||
`Login request is missing required resources: ${missingResources.join(
|
||||
", ",
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the signing address is the claimed wallet address
|
||||
const message = this.generateMessage(payload.payload);
|
||||
const chainId =
|
||||
this.wallet.type === "evm" && payload.payload.chain_id
|
||||
? parseInt(payload.payload.chain_id)
|
||||
: undefined;
|
||||
const verified = await this.verifySignature(
|
||||
message,
|
||||
payload.signature,
|
||||
payload.payload.address,
|
||||
chainId,
|
||||
);
|
||||
if (!verified) {
|
||||
throw new Error(
|
||||
`Signer address does not match payload address '${payload.payload.address.toLowerCase()}'`,
|
||||
);
|
||||
}
|
||||
|
||||
return payload.payload.address;
|
||||
}
|
||||
|
||||
public async generate(
|
||||
payload: LoginPayload,
|
||||
options?: GenerateOptions,
|
||||
): Promise<string> {
|
||||
if (isBrowser()) {
|
||||
throw new Error(
|
||||
"Authentication tokens should not be generated in the browser, as they must be signed by a server-side admin wallet.",
|
||||
);
|
||||
}
|
||||
|
||||
const parsedOptions = GenerateOptionsSchema.parse(options);
|
||||
|
||||
const domain = parsedOptions?.domain || this.domain;
|
||||
const userAddress = await this.verify(payload, {
|
||||
domain,
|
||||
...parsedOptions?.verifyOptions,
|
||||
});
|
||||
|
||||
let session: Json | undefined = undefined;
|
||||
if (typeof parsedOptions?.session === "function") {
|
||||
const sessionTrigger = (await parsedOptions.session(userAddress)) as Json;
|
||||
if (sessionTrigger) {
|
||||
session = sessionTrigger;
|
||||
}
|
||||
} else {
|
||||
session = parsedOptions?.session;
|
||||
}
|
||||
|
||||
const adminAddress = await this.wallet.getAddress();
|
||||
const payloadData = AuthenticationPayloadDataSchema.parse({
|
||||
iss: adminAddress,
|
||||
sub: userAddress,
|
||||
aud: domain,
|
||||
nbf: parsedOptions?.invalidBefore || new Date(),
|
||||
exp:
|
||||
parsedOptions?.expirationTime ||
|
||||
new Date(Date.now() + 1000 * 60 * 60 * 5),
|
||||
iat: new Date(),
|
||||
jti: parsedOptions?.tokenId,
|
||||
ctx: session,
|
||||
});
|
||||
|
||||
const message = JSON.stringify(payloadData);
|
||||
const signature = await this.wallet.signMessage(message);
|
||||
|
||||
// Header used for JWT token specifying hash algorithm
|
||||
const header = {
|
||||
// Specify ECDSA with SHA-256 for hashing algorithm
|
||||
alg: "ES256",
|
||||
typ: "JWT",
|
||||
};
|
||||
|
||||
const encodedHeader = Buffer.from(JSON.stringify(header)).toString(
|
||||
"base64",
|
||||
);
|
||||
const encodedData = Buffer.from(JSON.stringify(payloadData))
|
||||
.toString("base64")
|
||||
.replace(/=/g, "");
|
||||
const encodedSignature = Buffer.from(signature).toString("base64");
|
||||
|
||||
// Generate a JWT token with base64 encoded header, payload, and signature
|
||||
const token = `${encodedHeader}.${encodedData}.${encodedSignature}`;
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate With Token
|
||||
* @remarks Server-side function that authenticates the provided JWT token. This function verifies that
|
||||
* the provided authentication token is valid and returns the address of the authenticated wallet.
|
||||
*
|
||||
* @param domain - The domain of the server-side application doing authentication
|
||||
* @param token - The authentication token being used
|
||||
* @returns The address of the authenticated wallet
|
||||
*
|
||||
* @example
|
||||
* ```javascript
|
||||
* const domain = "example.com";
|
||||
* const loginPayload = await sdk.auth.login(domain);
|
||||
* const token = await sdk.auth.generateAuthToken(domain, loginPayload);
|
||||
*
|
||||
* // Authenticate the token and get the address of authenticating users wallet
|
||||
* const address = sdk.auth.authenticate(domain, token);
|
||||
* ```
|
||||
*/
|
||||
public async authenticate<TSession extends Json = Json>(
|
||||
token: string,
|
||||
options?: AuthenticateOptions,
|
||||
): Promise<User<TSession>> {
|
||||
if (isBrowser()) {
|
||||
throw new Error(
|
||||
"Should not authenticate tokens in the browser, as they must be verified by the server-side admin wallet.",
|
||||
);
|
||||
}
|
||||
|
||||
const parsedOptions = AuthenticateOptionsSchema.parse(options);
|
||||
const domain = parsedOptions?.domain || this.domain;
|
||||
|
||||
const encodedPayload = token.split(".")[1];
|
||||
const encodedSignature = token.split(".")[2];
|
||||
const payload: AuthenticationPayloadData = JSON.parse(
|
||||
Buffer.from(encodedPayload, "base64").toString(),
|
||||
);
|
||||
const signature = Buffer.from(encodedSignature, "base64").toString();
|
||||
|
||||
// Check that the payload unique ID is valid
|
||||
if (parsedOptions?.validateTokenId !== undefined) {
|
||||
try {
|
||||
await parsedOptions.validateTokenId(payload.jti);
|
||||
} catch (err) {
|
||||
throw new Error(`Token ID is invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the token audience matches the domain
|
||||
if (payload.aud !== domain) {
|
||||
throw new Error(
|
||||
`Expected token to be for the domain '${domain}', but found token with domain '${payload.aud}'`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check that the token is past the invalid before time
|
||||
const currentTime = Math.floor(new Date().getTime() / 1000);
|
||||
if (currentTime < payload.nbf) {
|
||||
throw new Error(
|
||||
`This token is invalid before epoch time '${payload.nbf}', current epoch time is '${currentTime}'`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check that the token hasn't expired
|
||||
if (currentTime > payload.exp) {
|
||||
throw new Error(
|
||||
`This token expired at epoch time '${payload.exp}', current epoch time is '${currentTime}'`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check that the connected wallet matches the token issuer
|
||||
const connectedAddress = await this.wallet.getAddress();
|
||||
if (connectedAddress.toLowerCase() !== payload.iss.toLowerCase()) {
|
||||
throw new Error(
|
||||
`Expected the connected wallet address '${connectedAddress}' to match the token issuer address '${payload.iss}'`,
|
||||
);
|
||||
}
|
||||
|
||||
let chainId: number | undefined = undefined;
|
||||
if (this.wallet.getChainId) {
|
||||
try {
|
||||
chainId = await this.wallet.getChainId();
|
||||
} catch {
|
||||
// ignore error
|
||||
}
|
||||
}
|
||||
|
||||
const verified = await this.verifySignature(
|
||||
JSON.stringify(payload),
|
||||
signature,
|
||||
connectedAddress,
|
||||
chainId,
|
||||
);
|
||||
if (!verified) {
|
||||
throw new Error(
|
||||
`The connected wallet address '${connectedAddress}' did not sign the token`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
address: payload.sub,
|
||||
session: payload.ctx as TSession | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private async verifySignature(
|
||||
message: string,
|
||||
signature: string,
|
||||
address: string,
|
||||
chainId?: number,
|
||||
) {
|
||||
return this.wallet.verifySignature(message, signature, address, chainId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a EIP-4361 & CAIP-122 compliant message to sign based on the login payload
|
||||
*/
|
||||
private generateMessage(payload: LoginPayloadData): string {
|
||||
const typeField = payload.type === "evm" ? "Ethereum" : "Solana";
|
||||
const header = `${payload.domain} wants you to sign in with your ${typeField} account:`;
|
||||
let prefix = [header, payload.address].join("\n");
|
||||
prefix = [prefix, payload.statement].join("\n\n");
|
||||
if (payload.statement) {
|
||||
prefix += "\n";
|
||||
}
|
||||
|
||||
const suffixArray = [];
|
||||
if (payload.uri) {
|
||||
const uriField = `URI: ${payload.uri}`;
|
||||
suffixArray.push(uriField);
|
||||
}
|
||||
|
||||
const versionField = `Version: ${payload.version}`;
|
||||
suffixArray.push(versionField);
|
||||
|
||||
if (payload.chain_id) {
|
||||
const chainField = `Chain ID: ` + payload.chain_id || "1";
|
||||
suffixArray.push(chainField);
|
||||
}
|
||||
|
||||
const nonceField = `Nonce: ${payload.nonce}`;
|
||||
suffixArray.push(nonceField);
|
||||
|
||||
const issuedAtField = `Issued At: ${payload.issued_at}`;
|
||||
suffixArray.push(issuedAtField);
|
||||
|
||||
const expiryField = `Expiration Time: ${payload.expiration_time}`;
|
||||
suffixArray.push(expiryField);
|
||||
|
||||
if (payload.invalid_before) {
|
||||
const invalidBeforeField = `Not Before: ${payload.invalid_before}`;
|
||||
suffixArray.push(invalidBeforeField);
|
||||
}
|
||||
|
||||
if (payload.resources) {
|
||||
suffixArray.push(
|
||||
[`Resources:`, ...payload.resources.map((x) => `- ${x}`)].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
const suffix = suffixArray.join("\n");
|
||||
return [prefix, suffix].join("\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { ThirdwebAuth } from "./auth";
|
||||
export * from "./schema";
|
||||
@@ -0,0 +1,208 @@
|
||||
import { utils, BigNumber } from "ethers";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
export const AddressSchema = z.string().refine(
|
||||
(arg) => utils.isAddress(arg),
|
||||
(out) => {
|
||||
return {
|
||||
message: `${out} is not a valid address`,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const RawDateSchema = z.date().transform((i) => {
|
||||
return BigNumber.from(Math.floor(i.getTime() / 1000));
|
||||
});
|
||||
|
||||
export const AccountTypeSchema = z.union([
|
||||
z.literal("evm"),
|
||||
z.literal("solana"),
|
||||
]);
|
||||
|
||||
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
||||
type Literal = z.infer<typeof literalSchema>;
|
||||
export type Json = Literal | { [key: string]: Json } | Json[];
|
||||
const JsonSchema: z.ZodType<Json> = z.lazy(
|
||||
() => z.union([literalSchema, z.array(JsonSchema), z.record(JsonSchema)]),
|
||||
{ invalid_type_error: "Provided value was not valid JSON" },
|
||||
);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const LoginOptionsSchema = z
|
||||
.object({
|
||||
domain: z.string().optional(),
|
||||
statement: z.string().optional(),
|
||||
uri: z.string().optional(),
|
||||
version: z.string().optional(),
|
||||
chainId: z.string().optional(),
|
||||
nonce: z.string().optional(),
|
||||
expirationTime: z.date().optional(),
|
||||
invalidBefore: z.date().optional(),
|
||||
resources: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const LoginPayloadDataSchema = z.object({
|
||||
type: AccountTypeSchema,
|
||||
domain: z.string(),
|
||||
address: z.string(),
|
||||
statement: z
|
||||
.string()
|
||||
.default(
|
||||
"Please ensure that the domain above matches the URL of the current website.",
|
||||
),
|
||||
uri: z.string().optional(),
|
||||
version: z.string().default("1"),
|
||||
chain_id: z.string().optional(),
|
||||
nonce: z.string().default(uuidv4()),
|
||||
issued_at: z
|
||||
.date()
|
||||
.default(new Date())
|
||||
.transform((d) => d.toISOString()),
|
||||
expiration_time: z.date().transform((d) => d.toISOString()),
|
||||
invalid_before: z
|
||||
.date()
|
||||
.default(new Date())
|
||||
.transform((d) => d.toISOString()),
|
||||
resources: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const LoginPayloadSchema = z.object({
|
||||
payload: LoginPayloadDataSchema,
|
||||
signature: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
const VerifyOptionsSchemaRequired = z.object({
|
||||
domain: z.string().optional(),
|
||||
statement: z.string().optional(),
|
||||
uri: z.string().optional(),
|
||||
version: z.string().optional(),
|
||||
chainId: z.string().optional(),
|
||||
validateNonce: z.function().args(z.string()).optional(),
|
||||
resources: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const VerifyOptionsSchema = VerifyOptionsSchemaRequired.optional();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const GenerateOptionsSchema = z
|
||||
.object({
|
||||
domain: z.string().optional(),
|
||||
tokenId: z.string().optional(),
|
||||
expirationTime: z.date().optional(),
|
||||
invalidBefore: z.date().optional(),
|
||||
session: z.union([JsonSchema, z.function().args(z.string())]).optional(),
|
||||
verifyOptions: VerifyOptionsSchemaRequired.omit({
|
||||
domain: true,
|
||||
}).optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const AuthenticationPayloadDataSchema = z.object({
|
||||
iss: z.string(),
|
||||
sub: z.string(),
|
||||
aud: z.string(),
|
||||
exp: RawDateSchema.transform((b) => b.toNumber()),
|
||||
nbf: RawDateSchema.transform((b) => b.toNumber()),
|
||||
iat: RawDateSchema.transform((b) => b.toNumber()),
|
||||
jti: z.string().default(uuidv4()),
|
||||
ctx: JsonSchema.optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const AuthenticationPayloadSchema = z.object({
|
||||
payload: AuthenticationPayloadDataSchema,
|
||||
signature: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const AuthenticateOptionsSchema = z
|
||||
.object({
|
||||
domain: z.string().optional(),
|
||||
validateTokenId: z.function().args(z.string()).optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type LoginOptions = z.input<typeof LoginOptionsSchema>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type LoginPayloadData = z.output<typeof LoginPayloadDataSchema>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type LoginPayload = z.output<typeof LoginPayloadSchema>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type VerifyOptions = z.input<typeof VerifyOptionsSchema>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type GenerateOptions = z.input<typeof GenerateOptionsSchema>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type AuthenticationPayloadData = z.output<
|
||||
typeof AuthenticationPayloadDataSchema
|
||||
>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type AuthenticationPayload = z.output<
|
||||
typeof AuthenticationPayloadSchema
|
||||
>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type AuthenticateOptions = z.output<typeof AuthenticateOptionsSchema>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type User<TContext extends Json = Json> = {
|
||||
address: string;
|
||||
session?: TContext;
|
||||
};
|
||||
|
||||
export const LoginPayloadOutputSchema = LoginPayloadSchema.extend({
|
||||
payload: LoginPayloadDataSchema.extend({
|
||||
issued_at: z.string(),
|
||||
expiration_time: z.string(),
|
||||
invalid_before: z.string(),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const isBrowser = () => typeof window !== "undefined";
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const isNode = () => !isBrowser();
|
||||
@@ -0,0 +1,613 @@
|
||||
import type { Chain } from "@wagmi/core";
|
||||
|
||||
const arbitrum: Chain = {
|
||||
id: 42_161,
|
||||
name: "Arbitrum One",
|
||||
network: "arbitrum",
|
||||
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
||||
rpcUrls: {
|
||||
alchemy: {
|
||||
http: ["https://arb-mainnet.g.alchemy.com/v2"],
|
||||
webSocket: ["wss://arb-mainnet.g.alchemy.com/v2"],
|
||||
},
|
||||
infura: {
|
||||
http: ["https://arbitrum-mainnet.infura.io/v3"],
|
||||
webSocket: ["wss://arbitrum-mainnet.infura.io/ws/v3"],
|
||||
},
|
||||
default: {
|
||||
http: ["https://arb1.arbitrum.io/rpc"],
|
||||
},
|
||||
public: {
|
||||
http: ["https://arb1.arbitrum.io/rpc"],
|
||||
},
|
||||
},
|
||||
blockExplorers: {
|
||||
etherscan: { name: "Arbiscan", url: "https://arbiscan.io" },
|
||||
default: { name: "Arbiscan", url: "https://arbiscan.io" },
|
||||
},
|
||||
contracts: {
|
||||
multicall3: {
|
||||
address: "0xca11bde05977b3631167028862be2a173976ca11",
|
||||
blockCreated: 7654707,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const arbitrumGoerli: Chain = {
|
||||
id: 421_613,
|
||||
name: "Arbitrum Goerli",
|
||||
network: "arbitrum-goerli",
|
||||
nativeCurrency: {
|
||||
name: "Arbitrum Goerli Ether",
|
||||
symbol: "ETH",
|
||||
decimals: 18,
|
||||
},
|
||||
rpcUrls: {
|
||||
alchemy: {
|
||||
http: ["https://arb-goerli.g.alchemy.com/v2"],
|
||||
webSocket: ["wss://arb-goerli.g.alchemy.com/v2"],
|
||||
},
|
||||
infura: {
|
||||
http: ["https://arbitrum-goerli.infura.io/v3"],
|
||||
webSocket: ["wss://arbitrum-goerli.infura.io/ws/v3"],
|
||||
},
|
||||
default: {
|
||||
http: ["https://goerli-rollup.arbitrum.io/rpc"],
|
||||
},
|
||||
public: {
|
||||
http: ["https://goerli-rollup.arbitrum.io/rpc"],
|
||||
},
|
||||
},
|
||||
blockExplorers: {
|
||||
etherscan: { name: "Arbiscan", url: "https://goerli.arbiscan.io/" },
|
||||
default: { name: "Arbiscan", url: "https://goerli.arbiscan.io/" },
|
||||
},
|
||||
contracts: {
|
||||
multicall3: {
|
||||
address: "0xca11bde05977b3631167028862be2a173976ca11",
|
||||
blockCreated: 88114,
|
||||
},
|
||||
},
|
||||
testnet: true,
|
||||
};
|
||||
|
||||
const avalanche: Chain = {
|
||||
id: 43_114,
|
||||
name: "Avalanche",
|
||||
network: "avalanche",
|
||||
nativeCurrency: {
|
||||
decimals: 18,
|
||||
name: "Avalanche",
|
||||
symbol: "AVAX",
|
||||
},
|
||||
rpcUrls: {
|
||||
default: { http: ["https://api.avax.network/ext/bc/C/rpc"] },
|
||||
public: { http: ["https://api.avax.network/ext/bc/C/rpc"] },
|
||||
},
|
||||
blockExplorers: {
|
||||
etherscan: { name: "SnowTrace", url: "https://snowtrace.io" },
|
||||
default: { name: "SnowTrace", url: "https://snowtrace.io" },
|
||||
},
|
||||
contracts: {
|
||||
multicall3: {
|
||||
address: "0xca11bde05977b3631167028862be2a173976ca11",
|
||||
blockCreated: 11907934,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const avalancheFuji: Chain = {
|
||||
id: 43_113,
|
||||
name: "Avalanche Fuji",
|
||||
network: "avalanche-fuji",
|
||||
nativeCurrency: {
|
||||
decimals: 18,
|
||||
name: "Avalanche Fuji",
|
||||
symbol: "AVAX",
|
||||
},
|
||||
rpcUrls: {
|
||||
default: { http: ["https://api.avax-test.network/ext/bc/C/rpc"] },
|
||||
public: { http: ["https://api.avax-test.network/ext/bc/C/rpc"] },
|
||||
},
|
||||
blockExplorers: {
|
||||
etherscan: { name: "SnowTrace", url: "https://testnet.snowtrace.io" },
|
||||
default: { name: "SnowTrace", url: "https://testnet.snowtrace.io" },
|
||||
},
|
||||
contracts: {
|
||||
multicall3: {
|
||||
address: "0xca11bde05977b3631167028862be2a173976ca11",
|
||||
blockCreated: 7096959,
|
||||
},
|
||||
},
|
||||
testnet: true,
|
||||
};
|
||||
|
||||
const bsc: Chain = {
|
||||
id: 56,
|
||||
name: "BNB Smart Chain",
|
||||
network: "bsc",
|
||||
nativeCurrency: {
|
||||
decimals: 18,
|
||||
name: "BNB",
|
||||
symbol: "BNB",
|
||||
},
|
||||
rpcUrls: {
|
||||
default: { http: ["https://rpc.ankr.com/bsc"] },
|
||||
public: { http: ["https://rpc.ankr.com/bsc"] },
|
||||
},
|
||||
blockExplorers: {
|
||||
etherscan: { name: "BscScan", url: "https://bscscan.com" },
|
||||
default: { name: "BscScan", url: "https://bscscan.com" },
|
||||
},
|
||||
contracts: {
|
||||
multicall3: {
|
||||
address: "0xca11bde05977b3631167028862be2a173976ca11",
|
||||
blockCreated: 15921452,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bscTestnet: Chain = {
|
||||
id: 97,
|
||||
name: "Binance Smart Chain Testnet",
|
||||
network: "bsc-testnet",
|
||||
nativeCurrency: {
|
||||
decimals: 18,
|
||||
name: "BNB",
|
||||
symbol: "tBNB",
|
||||
},
|
||||
rpcUrls: {
|
||||
default: { http: ["https://bsc-testnet.public.blastapi.io"] },
|
||||
public: { http: ["https://bsc-testnet.public.blastapi.io"] },
|
||||
},
|
||||
blockExplorers: {
|
||||
etherscan: { name: "BscScan", url: "https://testnet.bscscan.com" },
|
||||
default: { name: "BscScan", url: "https://testnet.bscscan.com" },
|
||||
},
|
||||
contracts: {
|
||||
multicall3: {
|
||||
address: "0xca11bde05977b3631167028862be2a173976ca11",
|
||||
blockCreated: 17422483,
|
||||
},
|
||||
},
|
||||
testnet: true,
|
||||
};
|
||||
|
||||
const fantom: Chain = {
|
||||
id: 250,
|
||||
name: "Fantom",
|
||||
network: "fantom",
|
||||
nativeCurrency: {
|
||||
decimals: 18,
|
||||
name: "Fantom",
|
||||
symbol: "FTM",
|
||||
},
|
||||
rpcUrls: {
|
||||
default: { http: ["https://rpc.ankr.com/fantom"] },
|
||||
public: { http: ["https://rpc.ankr.com/fantom"] },
|
||||
},
|
||||
blockExplorers: {
|
||||
etherscan: { name: "FTMScan", url: "https://ftmscan.com" },
|
||||
default: { name: "FTMScan", url: "https://ftmscan.com" },
|
||||
},
|
||||
contracts: {
|
||||
multicall3: {
|
||||
address: "0xca11bde05977b3631167028862be2a173976ca11",
|
||||
blockCreated: 33001987,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const fantomTestnet: Chain = {
|
||||
id: 4002,
|
||||
name: "Fantom Testnet",
|
||||
network: "fantom-testnet",
|
||||
nativeCurrency: {
|
||||
decimals: 18,
|
||||
name: "Fantom",
|
||||
symbol: "FTM",
|
||||
},
|
||||
rpcUrls: {
|
||||
default: { http: ["https://rpc.testnet.fantom.network"] },
|
||||
public: { http: ["https://rpc.testnet.fantom.network"] },
|
||||
},
|
||||
blockExplorers: {
|
||||
etherscan: { name: "FTMScan", url: "https://testnet.ftmscan.com" },
|
||||
default: { name: "FTMScan", url: "https://testnet.ftmscan.com" },
|
||||
},
|
||||
contracts: {
|
||||
multicall3: {
|
||||
address: "0xca11bde05977b3631167028862be2a173976ca11",
|
||||
blockCreated: 8328688,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const goerli: Chain = {
|
||||
id: 5,
|
||||
network: "goerli",
|
||||
name: "Goerli",
|
||||
nativeCurrency: { name: "Goerli Ether", symbol: "ETH", decimals: 18 },
|
||||
rpcUrls: {
|
||||
alchemy: {
|
||||
http: ["https://eth-goerli.g.alchemy.com/v2"],
|
||||
webSocket: ["wss://eth-goerli.g.alchemy.com/v2"],
|
||||
},
|
||||
infura: {
|
||||
http: ["https://goerli.infura.io/v3"],
|
||||
webSocket: ["wss://goerli.infura.io/ws/v3"],
|
||||
},
|
||||
default: {
|
||||
http: ["https://rpc.ankr.com/eth_goerli"],
|
||||
},
|
||||
public: {
|
||||
http: ["https://rpc.ankr.com/eth_goerli"],
|
||||
},
|
||||
},
|
||||
blockExplorers: {
|
||||
etherscan: {
|
||||
name: "Etherscan",
|
||||
url: "https://goerli.etherscan.io",
|
||||
},
|
||||
default: {
|
||||
name: "Etherscan",
|
||||
url: "https://goerli.etherscan.io",
|
||||
},
|
||||
},
|
||||
contracts: {
|
||||
ensRegistry: {
|
||||
address: "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e",
|
||||
},
|
||||
multicall3: {
|
||||
address: "0xca11bde05977b3631167028862be2a173976ca11",
|
||||
blockCreated: 6507670,
|
||||
},
|
||||
},
|
||||
testnet: true,
|
||||
};
|
||||
|
||||
const mainnet: Chain = {
|
||||
id: 1,
|
||||
network: "homestead",
|
||||
name: "Ethereum",
|
||||
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
||||
rpcUrls: {
|
||||
alchemy: {
|
||||
http: ["https://eth-mainnet.g.alchemy.com/v2"],
|
||||
webSocket: ["wss://eth-mainnet.g.alchemy.com/v2"],
|
||||
},
|
||||
infura: {
|
||||
http: ["https://mainnet.infura.io/v3"],
|
||||
webSocket: ["wss://mainnet.infura.io/ws/v3"],
|
||||
},
|
||||
default: {
|
||||
http: ["https://cloudflare-eth.com"],
|
||||
},
|
||||
public: {
|
||||
http: ["https://cloudflare-eth.com"],
|
||||
},
|
||||
},
|
||||
blockExplorers: {
|
||||
etherscan: {
|
||||
name: "Etherscan",
|
||||
url: "https://etherscan.io",
|
||||
},
|
||||
default: {
|
||||
name: "Etherscan",
|
||||
url: "https://etherscan.io",
|
||||
},
|
||||
},
|
||||
contracts: {
|
||||
ensRegistry: {
|
||||
address: "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e",
|
||||
},
|
||||
multicall3: {
|
||||
address: "0xca11bde05977b3631167028862be2a173976ca11",
|
||||
blockCreated: 14353601,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const optimism: Chain = {
|
||||
id: 10,
|
||||
name: "Optimism",
|
||||
network: "optimism",
|
||||
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
||||
rpcUrls: {
|
||||
alchemy: {
|
||||
http: ["https://opt-mainnet.g.alchemy.com/v2"],
|
||||
webSocket: ["wss://opt-mainnet.g.alchemy.com/v2"],
|
||||
},
|
||||
infura: {
|
||||
http: ["https://optimism-mainnet.infura.io/v3"],
|
||||
webSocket: ["wss://optimism-mainnet.infura.io/ws/v3"],
|
||||
},
|
||||
default: {
|
||||
http: ["https://mainnet.optimism.io"],
|
||||
},
|
||||
public: {
|
||||
http: ["https://mainnet.optimism.io"],
|
||||
},
|
||||
},
|
||||
blockExplorers: {
|
||||
etherscan: {
|
||||
name: "Etherscan",
|
||||
url: "https://optimistic.etherscan.io",
|
||||
},
|
||||
default: {
|
||||
name: "Etherscan",
|
||||
url: "https://optimistic.etherscan.io",
|
||||
},
|
||||
},
|
||||
contracts: {
|
||||
multicall3: {
|
||||
address: "0xca11bde05977b3631167028862be2a173976ca11",
|
||||
blockCreated: 4286263,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const optimismGoerli: Chain = {
|
||||
id: 420,
|
||||
name: "Optimism Goerli",
|
||||
network: "optimism-goerli",
|
||||
nativeCurrency: { name: "Goerli Ether", symbol: "ETH", decimals: 18 },
|
||||
rpcUrls: {
|
||||
alchemy: {
|
||||
http: ["https://opt-goerli.g.alchemy.com/v2"],
|
||||
webSocket: ["wss://opt-goerli.g.alchemy.com/v2"],
|
||||
},
|
||||
infura: {
|
||||
http: ["https://optimism-goerli.infura.io/v3"],
|
||||
webSocket: ["wss://optimism-goerli.infura.io/ws/v3"],
|
||||
},
|
||||
default: {
|
||||
http: ["https://goerli.optimism.io"],
|
||||
},
|
||||
public: {
|
||||
http: ["https://goerli.optimism.io"],
|
||||
},
|
||||
},
|
||||
blockExplorers: {
|
||||
etherscan: {
|
||||
name: "Etherscan",
|
||||
url: "https://goerli-optimism.etherscan.io",
|
||||
},
|
||||
default: {
|
||||
name: "Etherscan",
|
||||
url: "https://goerli-optimism.etherscan.io",
|
||||
},
|
||||
},
|
||||
contracts: {
|
||||
multicall3: {
|
||||
address: "0xca11bde05977b3631167028862be2a173976ca11",
|
||||
blockCreated: 49461,
|
||||
},
|
||||
},
|
||||
testnet: true,
|
||||
};
|
||||
|
||||
const polygon: Chain = {
|
||||
id: 137,
|
||||
name: "Polygon",
|
||||
network: "matic",
|
||||
nativeCurrency: { name: "MATIC", symbol: "MATIC", decimals: 18 },
|
||||
rpcUrls: {
|
||||
alchemy: {
|
||||
http: ["https://polygon-mainnet.g.alchemy.com/v2"],
|
||||
webSocket: ["wss://polygon-mainnet.g.alchemy.com/v2"],
|
||||
},
|
||||
infura: {
|
||||
http: ["https://polygon-mainnet.infura.io/v3"],
|
||||
webSocket: ["wss://polygon-mainnet.infura.io/ws/v3"],
|
||||
},
|
||||
default: {
|
||||
http: ["https://polygon-rpc.com"],
|
||||
},
|
||||
public: {
|
||||
http: ["https://polygon-rpc.com"],
|
||||
},
|
||||
},
|
||||
blockExplorers: {
|
||||
etherscan: {
|
||||
name: "PolygonScan",
|
||||
url: "https://polygonscan.com",
|
||||
},
|
||||
default: {
|
||||
name: "PolygonScan",
|
||||
url: "https://polygonscan.com",
|
||||
},
|
||||
},
|
||||
contracts: {
|
||||
multicall3: {
|
||||
address: "0xca11bde05977b3631167028862be2a173976ca11",
|
||||
blockCreated: 25770160,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const polygonMumbai: Chain = {
|
||||
id: 80_001,
|
||||
name: "Polygon Mumbai",
|
||||
network: "maticmum",
|
||||
nativeCurrency: { name: "MATIC", symbol: "MATIC", decimals: 18 },
|
||||
rpcUrls: {
|
||||
alchemy: {
|
||||
http: ["https://polygon-mumbai.g.alchemy.com/v2"],
|
||||
webSocket: ["wss://polygon-mumbai.g.alchemy.com/v2"],
|
||||
},
|
||||
infura: {
|
||||
http: ["https://polygon-mumbai.infura.io/v3"],
|
||||
webSocket: ["wss://polygon-mumbai.infura.io/ws/v3"],
|
||||
},
|
||||
default: {
|
||||
http: ["https://matic-mumbai.chainstacklabs.com"],
|
||||
},
|
||||
public: {
|
||||
http: ["https://matic-mumbai.chainstacklabs.com"],
|
||||
},
|
||||
},
|
||||
blockExplorers: {
|
||||
etherscan: {
|
||||
name: "PolygonScan",
|
||||
url: "https://mumbai.polygonscan.com",
|
||||
},
|
||||
default: {
|
||||
name: "PolygonScan",
|
||||
url: "https://mumbai.polygonscan.com",
|
||||
},
|
||||
},
|
||||
contracts: {
|
||||
multicall3: {
|
||||
address: "0xca11bde05977b3631167028862be2a173976ca11",
|
||||
blockCreated: 25770160,
|
||||
},
|
||||
},
|
||||
testnet: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* @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,
|
||||
];
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const NATIVE_TOKEN_ADDRESS =
|
||||
"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
||||
|
||||
export const DEFAULT_API_KEY =
|
||||
"c6634ad2d97b74baf15ff556016830c251050e6c36b9da508ce3ec80095d3dc1";
|
||||
|
||||
function getRpcNameFromChainId(chainId: SUPPORTED_CHAIN_ID): string {
|
||||
switch (chainId) {
|
||||
case ChainId.Mainnet:
|
||||
return "mainnet";
|
||||
case ChainId.Goerli:
|
||||
return "goerli";
|
||||
case ChainId.Polygon:
|
||||
return "polygon";
|
||||
case ChainId.Mumbai:
|
||||
return "mumbai";
|
||||
case ChainId.Avalanche:
|
||||
return "avalanche";
|
||||
case ChainId.AvalancheFujiTestnet:
|
||||
return "avalanche-fuji";
|
||||
case ChainId.Fantom:
|
||||
return "fantom";
|
||||
case ChainId.FantomTestnet:
|
||||
return "fantom-testnet";
|
||||
case ChainId.Arbitrum:
|
||||
return "arbitrum";
|
||||
case ChainId.ArbitrumGoerli:
|
||||
return "arbitrum-goerli";
|
||||
case ChainId.Optimism:
|
||||
return "optimism";
|
||||
case ChainId.OptimismGoerli:
|
||||
return "optimism-goerli";
|
||||
case ChainId.BinanceSmartChainMainnet:
|
||||
return "bsc";
|
||||
case ChainId.BinanceSmartChainTestnet:
|
||||
return "bsc-testnet";
|
||||
default:
|
||||
throw new Error("Unsupported chain id");
|
||||
}
|
||||
}
|
||||
|
||||
export function getRpcUrl(chainId: SUPPORTED_CHAIN_ID) {
|
||||
return `https://${getRpcNameFromChainId(
|
||||
chainId,
|
||||
)}.rpc.thirdweb.com/${DEFAULT_API_KEY}`;
|
||||
}
|
||||
|
||||
function enhanceChain<TChain extends Chain>(chain: TChain) {
|
||||
const twRPC = getRpcUrl(chain.id);
|
||||
return {
|
||||
...chain,
|
||||
rpcUrls: {
|
||||
...chain.rpcUrls,
|
||||
default: {
|
||||
...chain.rpcUrls.default,
|
||||
http: [twRPC, ...chain.rpcUrls.default.http],
|
||||
},
|
||||
public: {
|
||||
...chain.rpcUrls.public,
|
||||
http: [twRPC, ...(chain.rpcUrls.public?.http || [])],
|
||||
},
|
||||
},
|
||||
} as TChain;
|
||||
}
|
||||
|
||||
export const supportedChains: Record<SUPPORTED_CHAIN_ID, Chain> = {
|
||||
[ChainId.Mainnet]: enhanceChain(mainnet),
|
||||
[ChainId.Goerli]: enhanceChain(goerli),
|
||||
[ChainId.Polygon]: enhanceChain(polygon),
|
||||
[ChainId.Mumbai]: enhanceChain(polygonMumbai),
|
||||
[ChainId.Avalanche]: enhanceChain(avalanche),
|
||||
[ChainId.AvalancheFujiTestnet]: enhanceChain(avalancheFuji),
|
||||
[ChainId.Fantom]: enhanceChain(fantom),
|
||||
[ChainId.FantomTestnet]: enhanceChain(fantomTestnet),
|
||||
[ChainId.Arbitrum]: enhanceChain(arbitrum),
|
||||
[ChainId.ArbitrumGoerli]: enhanceChain(arbitrumGoerli),
|
||||
[ChainId.Optimism]: enhanceChain(optimism),
|
||||
[ChainId.OptimismGoerli]: enhanceChain(optimismGoerli),
|
||||
[ChainId.BinanceSmartChainMainnet]: enhanceChain(bsc),
|
||||
[ChainId.BinanceSmartChainTestnet]: enhanceChain(bscTestnet),
|
||||
};
|
||||
|
||||
export const thirdwebChains: Chain[] = Object.values(supportedChains);
|
||||
@@ -0,0 +1,93 @@
|
||||
import { SUPPORTED_CHAIN_ID, supportedChains } from "./evm";
|
||||
import type { Ecosystem, GenericAuthWallet } from "@thirdweb-dev/wallets";
|
||||
import { ethers } from "ethers";
|
||||
|
||||
const EIP1271_ABI = [
|
||||
"function isValidSignature(bytes32 _message, bytes _signature) public view returns (bytes4)",
|
||||
];
|
||||
const EIP1271_MAGICVALUE = "0x1626ba7e";
|
||||
|
||||
export const checkContractWalletSignature = async (
|
||||
message: string,
|
||||
signature: string,
|
||||
address: string,
|
||||
chainId: number,
|
||||
): Promise<boolean> => {
|
||||
const rpcUrl =
|
||||
supportedChains[chainId as SUPPORTED_CHAIN_ID]?.rpcUrls.default.http[0];
|
||||
if (!rpcUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
|
||||
const walletContract = new ethers.Contract(address, EIP1271_ABI, provider);
|
||||
const hashMessage = ethers.utils.hashMessage(message);
|
||||
try {
|
||||
const res = await walletContract.isValidSignature(hashMessage, signature);
|
||||
return res === EIP1271_MAGICVALUE;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export class SignerWallet implements GenericAuthWallet {
|
||||
type: Ecosystem = "evm";
|
||||
#signer: ethers.Signer;
|
||||
|
||||
constructor(signer: ethers.Signer) {
|
||||
this.#signer = signer;
|
||||
}
|
||||
|
||||
public async getAddress(): Promise<string> {
|
||||
return this.#signer.getAddress();
|
||||
}
|
||||
|
||||
public async getChainId(): Promise<number> {
|
||||
return this.#signer.getChainId();
|
||||
}
|
||||
|
||||
public async signMessage(message: string): Promise<string> {
|
||||
return await this.#signer.signMessage(message);
|
||||
}
|
||||
|
||||
public async verifySignature(
|
||||
message: string,
|
||||
signature: string,
|
||||
address: string,
|
||||
chainId?: number,
|
||||
): Promise<boolean> {
|
||||
const messageHash = ethers.utils.hashMessage(message);
|
||||
const messageHashBytes = ethers.utils.arrayify(messageHash);
|
||||
const recoveredAddress = ethers.utils.recoverAddress(
|
||||
messageHashBytes,
|
||||
signature,
|
||||
);
|
||||
|
||||
if (recoveredAddress === address) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the address is a smart contract wallet
|
||||
if (chainId !== undefined) {
|
||||
try {
|
||||
const isValid = await checkContractWalletSignature(
|
||||
message,
|
||||
signature,
|
||||
address,
|
||||
chainId || 1,
|
||||
);
|
||||
return isValid;
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class PrivateKeyWallet extends SignerWallet {
|
||||
constructor(privateKey: string) {
|
||||
super(new ethers.Wallet(privateKey));
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import loginHandler from "./routes/login";
|
||||
import logoutHandler from "./routes/logout";
|
||||
import userHandler from "./routes/user";
|
||||
import {
|
||||
ThirdwebAuthConfig,
|
||||
ThirdwebAuthRoute,
|
||||
ThirdwebAuthUser,
|
||||
} from "./types";
|
||||
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
|
||||
import cookieParser from "cookie-parser";
|
||||
import { Express, NextFunction, Request, Response } from "express";
|
||||
|
||||
export * from "./types";
|
||||
|
||||
export function getUser(req: Request): ThirdwebAuthUser | null {
|
||||
return req.user;
|
||||
}
|
||||
|
||||
export function ThirdwebAuth(app: Express, cfg: ThirdwebAuthConfig) {
|
||||
const ctx = {
|
||||
...cfg,
|
||||
sdk: ThirdwebSDK.fromPrivateKey(cfg.privateKey, "mainnet"),
|
||||
};
|
||||
|
||||
const authUrl = cfg.authUrl?.replace(/\/$/, "") || "/auth";
|
||||
|
||||
app.use(cookieParser());
|
||||
|
||||
app.use(async (req: Request, _: Response, next: NextFunction) => {
|
||||
const { sdk, domain } = ctx;
|
||||
let user = null;
|
||||
const token = req.cookies.thirdweb_auth_token;
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
const address = await sdk.auth.authenticate(domain, token);
|
||||
|
||||
if (ctx.callbacks?.user) {
|
||||
user = await ctx.callbacks.user(address);
|
||||
}
|
||||
|
||||
user = { ...user, address };
|
||||
} catch {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
|
||||
req.user = user as ThirdwebAuthUser | null;
|
||||
next();
|
||||
});
|
||||
|
||||
app.get(`${authUrl}/:route`, (req: Request, res: Response) => {
|
||||
const action = req.params.route as ThirdwebAuthRoute;
|
||||
|
||||
switch (action) {
|
||||
case "login":
|
||||
return loginHandler(req, res, ctx);
|
||||
case "user":
|
||||
return userHandler(req, res);
|
||||
case "logout":
|
||||
return logoutHandler(req, res);
|
||||
default:
|
||||
return res.status(400).json({
|
||||
message: "Invalid route for authentication.",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { ThirdwebAuthContext } from "../types";
|
||||
import { LoginPayload } from "@thirdweb-dev/sdk";
|
||||
import { serialize } from "cookie";
|
||||
import { Request, Response } from "express";
|
||||
|
||||
function redirectWithError(req: Request, res: Response, error: string) {
|
||||
const encodedError = encodeURIComponent(error);
|
||||
const url = new URL(req.headers.referer as string);
|
||||
url.searchParams.set("error", encodedError);
|
||||
return res.redirect(url.toString());
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: Request,
|
||||
res: Response,
|
||||
ctx: ThirdwebAuthContext,
|
||||
) {
|
||||
if (req.method !== "GET") {
|
||||
return redirectWithError(req, res, "INVALID_METHOD");
|
||||
}
|
||||
|
||||
const { sdk, domain } = ctx;
|
||||
|
||||
// Get signed login payload from the frontend
|
||||
const payload = JSON.parse(atob(req.query.payload as string)) as LoginPayload;
|
||||
if (!payload) {
|
||||
redirectWithError(req, res, "MISSING_LOGIN_PAYLOAD");
|
||||
}
|
||||
|
||||
let token;
|
||||
try {
|
||||
// Generate an access token with the SDK using the signed payload
|
||||
token = await sdk.auth.generateAuthToken(domain, payload);
|
||||
} catch {
|
||||
return redirectWithError(req, res, "INVALID_LOGIN_PAYLOAD");
|
||||
}
|
||||
|
||||
// Securely set httpOnly cookie on request to prevent XSS on frontend
|
||||
// And set path to / to enable thirdweb_auth_token usage on all endpoints
|
||||
res.setHeader(
|
||||
"Set-Cookie",
|
||||
serialize("thirdweb_auth_token", token, {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "none",
|
||||
}),
|
||||
);
|
||||
|
||||
if (ctx.callbacks?.login) {
|
||||
const address = sdk.auth.verify(domain, payload);
|
||||
await ctx.callbacks.login(address);
|
||||
}
|
||||
|
||||
return res.status(301).redirect(req.query.redirect as string);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { serialize } from "cookie";
|
||||
import { Request, Response } from "express";
|
||||
|
||||
export default async function handler(req: Request, res: Response) {
|
||||
if (req.method !== "GET") {
|
||||
return res.status(400).json({
|
||||
error: "Invalid method. Only GET supported.",
|
||||
});
|
||||
}
|
||||
|
||||
// Set the access token to 'none' and expire in 5 seconds
|
||||
res.setHeader(
|
||||
"Set-Cookie",
|
||||
serialize("thirdweb_auth_token", "", {
|
||||
path: "/",
|
||||
expires: new Date(Date.now() + 5 * 1000),
|
||||
}),
|
||||
);
|
||||
|
||||
return res.status(301).redirect(req.headers.referer as string);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
|
||||
export default async function handler(req: Request, res: Response) {
|
||||
if (req.method !== "GET") {
|
||||
return res.status(400).json({
|
||||
error: "Invalid method. Only GET supported.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json(req.user);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { ThirdwebAuthUser } from "..";
|
||||
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Express {
|
||||
export interface Request {
|
||||
user: ThirdwebAuthUser | null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
|
||||
import { Request } from "express";
|
||||
|
||||
export type ThirdwebAuthRoute = "login" | "user" | "logout";
|
||||
|
||||
export type ThirdwebAuthConfig = {
|
||||
privateKey: string;
|
||||
domain: string;
|
||||
authUrl?: string;
|
||||
callbacks?: {
|
||||
login?: (address: string) => Promise<void> | void;
|
||||
user?: (
|
||||
address: string,
|
||||
) =>
|
||||
| Promise<Omit<ThirdwebAuthUser, "address">>
|
||||
| Omit<ThirdwebAuthUser, "address">;
|
||||
};
|
||||
};
|
||||
|
||||
export type ThirdwebAuthContext = {
|
||||
sdk: ThirdwebSDK;
|
||||
domain: string;
|
||||
callbacks?: {
|
||||
login?: (address: string) => Promise<void> | void;
|
||||
user?: (
|
||||
address: string,
|
||||
) =>
|
||||
| Promise<Omit<ThirdwebAuthUser, "address">>
|
||||
| Omit<ThirdwebAuthUser, "address">;
|
||||
};
|
||||
};
|
||||
|
||||
export type ThirdwebAuthUser = {
|
||||
address: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type RequestWithUser = Request & {
|
||||
user: ThirdwebAuthUser | null;
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Json } from "../../core/schema";
|
||||
import { ThirdwebAuthContext, ThirdwebAuthUser } from "../types";
|
||||
import { Request } from "express";
|
||||
|
||||
function getToken(req: Request): string | undefined {
|
||||
if (req.headers["authorization"]) {
|
||||
const authorizationHeader = req.headers["authorization"].split(" ");
|
||||
if (authorizationHeader?.length === 2) {
|
||||
return authorizationHeader[1];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cookie: string | undefined = !req.cookies
|
||||
? undefined
|
||||
: typeof req.cookies.get === "function"
|
||||
? (req.cookies as any).get("thirdweb_auth_token")
|
||||
: (req.cookies as any).thirdweb_auth_token;
|
||||
|
||||
return cookie;
|
||||
}
|
||||
|
||||
export async function getUser<
|
||||
TData extends Json = Json,
|
||||
TSession extends Json = Json,
|
||||
>(
|
||||
req: Request,
|
||||
ctx: ThirdwebAuthContext<TData, TSession>,
|
||||
): Promise<ThirdwebAuthUser<TData, TSession> | null> {
|
||||
const token = getToken(req);
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let authenticatedUser: ThirdwebAuthUser<TData, TSession>;
|
||||
try {
|
||||
authenticatedUser = await ctx.auth.authenticate<TSession>(token, {
|
||||
validateTokenId: async (tokenId: string) => {
|
||||
if (ctx.authOptions?.validateTokenId) {
|
||||
await ctx.authOptions?.validateTokenId(tokenId);
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!ctx.callbacks?.onUser) {
|
||||
return authenticatedUser;
|
||||
}
|
||||
|
||||
const data = await ctx.callbacks.onUser(authenticatedUser);
|
||||
if (!data) {
|
||||
return authenticatedUser;
|
||||
}
|
||||
|
||||
return { ...authenticatedUser, data: data };
|
||||
}
|
||||
@@ -1 +1,63 @@
|
||||
export * from "./evm";
|
||||
import { Json, ThirdwebAuth as ThirdwebAuthSDK } from "../core";
|
||||
import { getUser } from "./helpers/user";
|
||||
import loginHandler from "./routes/login";
|
||||
import logoutHandler from "./routes/logout";
|
||||
import userHandler from "./routes/user";
|
||||
import { ThirdwebAuthConfig, ThirdwebAuthContext } from "./types";
|
||||
import cookieParser from "cookie-parser";
|
||||
import express, { Request, Response } from "express";
|
||||
|
||||
export * from "./types";
|
||||
|
||||
const asyncHandler =
|
||||
(fn: CallableFunction) =>
|
||||
(...args: any[]) => {
|
||||
const fnReturn = fn(...args);
|
||||
const next = args[args.length - 1];
|
||||
return Promise.resolve(fnReturn).catch(next);
|
||||
};
|
||||
|
||||
export function ThirdwebAuth<
|
||||
TData extends Json = Json,
|
||||
TSession extends Json = Json,
|
||||
>(cfg: ThirdwebAuthConfig<TData, TSession>) {
|
||||
const ctx = {
|
||||
...cfg,
|
||||
auth: new ThirdwebAuthSDK(cfg.wallet, cfg.domain),
|
||||
};
|
||||
|
||||
const router = express.Router();
|
||||
const cookieMiddleware = cookieParser();
|
||||
|
||||
router.use(express.json());
|
||||
router.use(cookieMiddleware);
|
||||
|
||||
router.post(
|
||||
"/login",
|
||||
asyncHandler((req: Request, res: Response) =>
|
||||
loginHandler(req, res, ctx as ThirdwebAuthContext),
|
||||
),
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/user",
|
||||
asyncHandler((req: Request, res: Response) =>
|
||||
userHandler(req, res, ctx as ThirdwebAuthContext),
|
||||
),
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/logout",
|
||||
asyncHandler((req: Request, res: Response) =>
|
||||
logoutHandler(req, res, ctx as ThirdwebAuthContext),
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
authRouter: router,
|
||||
authMiddleware: cookieMiddleware,
|
||||
getUser: (req: Request) => {
|
||||
return getUser<TData, TSession>(req, ctx);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { GenerateOptions } from "../../core";
|
||||
import { LoginPayloadBodySchema, ThirdwebAuthContext } from "../types";
|
||||
import { serialize } from "cookie";
|
||||
import { Request, Response } from "express";
|
||||
|
||||
export default async function handler(
|
||||
req: Request,
|
||||
res: Response,
|
||||
ctx: ThirdwebAuthContext,
|
||||
) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
const parsedPayload = LoginPayloadBodySchema.safeParse(req.body);
|
||||
|
||||
// Get signed login payload from the frontend
|
||||
if (!parsedPayload.success) {
|
||||
return res.status(400).json({ error: "Invalid login payload" });
|
||||
}
|
||||
|
||||
const payload = parsedPayload.data.payload;
|
||||
|
||||
const validateNonce = async (nonce: string) => {
|
||||
if (ctx.authOptions?.validateNonce) {
|
||||
await ctx.authOptions?.validateNonce(nonce);
|
||||
}
|
||||
};
|
||||
|
||||
const getSession = async (address: string) => {
|
||||
if (ctx.callbacks?.onLogin) {
|
||||
return ctx.callbacks.onLogin(address, req);
|
||||
}
|
||||
};
|
||||
|
||||
const expirationTime = ctx.authOptions?.tokenDurationInSeconds
|
||||
? new Date(Date.now() + 1000 * ctx.authOptions.tokenDurationInSeconds)
|
||||
: undefined;
|
||||
|
||||
const generateOptions: GenerateOptions = {
|
||||
verifyOptions: {
|
||||
statement: ctx.authOptions?.statement,
|
||||
uri: ctx.authOptions?.uri,
|
||||
version: ctx.authOptions?.version,
|
||||
chainId: ctx.authOptions?.chainId,
|
||||
validateNonce,
|
||||
resources: ctx.authOptions?.resources,
|
||||
},
|
||||
expirationTime,
|
||||
session: getSession,
|
||||
};
|
||||
|
||||
let token: string;
|
||||
try {
|
||||
// Generate an access token with the SDK using the signed payload
|
||||
token = await ctx.auth.generate(payload, generateOptions);
|
||||
} catch (err: any) {
|
||||
if (err.message) {
|
||||
return res.status(400).json({ error: err.message });
|
||||
} else if (typeof err === "string") {
|
||||
return res.status(400).json({ error: err });
|
||||
} else {
|
||||
return res.status(400).json({ error: "Invalid login payload" });
|
||||
}
|
||||
}
|
||||
|
||||
// Securely set httpOnly cookie on request to prevent XSS on frontend
|
||||
// And set path to / to enable thirdweb_auth_token usage on all endpoints
|
||||
res.setHeader(
|
||||
"Set-Cookie",
|
||||
serialize("thirdweb_auth_token", token, {
|
||||
domain: ctx.cookieOptions?.domain,
|
||||
path: ctx.cookieOptions?.path || "/",
|
||||
sameSite: ctx.cookieOptions?.sameSite || "none",
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
}),
|
||||
);
|
||||
|
||||
// Send token in body and as cookie for frontend and backend use cases
|
||||
return res.status(200).json({ token });
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { getUser } from "../helpers/user";
|
||||
import { ThirdwebAuthContext } from "../types";
|
||||
import { serialize } from "cookie";
|
||||
import { Request, Response } from "express";
|
||||
|
||||
export default async function handler(
|
||||
req: Request,
|
||||
res: Response,
|
||||
ctx: ThirdwebAuthContext,
|
||||
) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({
|
||||
error: "Invalid method. Only POST supported.",
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.callbacks?.onLogout) {
|
||||
const user = await getUser(req, ctx);
|
||||
if (user) {
|
||||
await ctx.callbacks.onLogout(user, req);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the access token to 'none' and expire in 5 seconds
|
||||
res.setHeader(
|
||||
"Set-Cookie",
|
||||
serialize("thirdweb_auth_token", "", {
|
||||
domain: ctx.cookieOptions?.domain,
|
||||
path: ctx.cookieOptions?.path || "/",
|
||||
expires: new Date(Date.now() + 5 * 1000),
|
||||
}),
|
||||
);
|
||||
|
||||
return res.status(200).json({ message: "Succesfully logged out" });
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { getUser } from "../helpers/user";
|
||||
import { ThirdwebAuthContext } from "../types";
|
||||
import { Request, Response } from "express";
|
||||
|
||||
export default async function handler(
|
||||
req: Request,
|
||||
res: Response,
|
||||
ctx: ThirdwebAuthContext,
|
||||
) {
|
||||
if (req.method !== "GET") {
|
||||
return res.status(400).json({
|
||||
error: "Invalid method. Only GET supported.",
|
||||
});
|
||||
}
|
||||
|
||||
const user = await getUser(req, ctx);
|
||||
return res.status(200).json(user);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { ThirdwebAuth } from "../../core";
|
||||
import { Json, LoginPayloadOutputSchema, User } from "../../core/schema";
|
||||
import { GenericAuthWallet } from "@thirdweb-dev/wallets";
|
||||
import { Request } from "express";
|
||||
import { z } from "zod";
|
||||
|
||||
export const LoginPayloadBodySchema = z.object({
|
||||
payload: LoginPayloadOutputSchema,
|
||||
});
|
||||
|
||||
export type ThirdwebAuthRoute = "login" | "user" | "logout";
|
||||
|
||||
export type ThirdwebAuthUser<
|
||||
TData extends Json = Json,
|
||||
TSession extends Json = Json,
|
||||
> = User<TSession> & {
|
||||
data?: TData;
|
||||
};
|
||||
|
||||
export type ThirdwebAuthConfig<
|
||||
TData extends Json = Json,
|
||||
TSession extends Json = Json,
|
||||
> = {
|
||||
domain: string;
|
||||
wallet: GenericAuthWallet;
|
||||
authOptions?: {
|
||||
statement?: string;
|
||||
uri?: string;
|
||||
version?: string;
|
||||
chainId?: string;
|
||||
resources?: string[];
|
||||
validateNonce?:
|
||||
| ((nonce: string) => void)
|
||||
| ((nonce: string) => Promise<void>);
|
||||
validateTokenId?:
|
||||
| ((tokenId: string) => void)
|
||||
| ((tokenId: string) => Promise<void>);
|
||||
tokenDurationInSeconds?: number;
|
||||
};
|
||||
cookieOptions?: {
|
||||
domain?: string;
|
||||
path?: string;
|
||||
sameSite?: "lax" | "strict" | "none";
|
||||
};
|
||||
callbacks?: {
|
||||
onLogin?:
|
||||
| ((address: string, req?: Request) => void | TSession)
|
||||
| ((address: string, req?: Request) => Promise<void | TSession>);
|
||||
onUser?:
|
||||
| ((user: User<TSession>, req?: Request) => void | TData)
|
||||
| ((user: User<TSession>, req?: Request) => Promise<void | TData>);
|
||||
onLogout?:
|
||||
| ((user: User, req?: Request) => void)
|
||||
| ((user: User, req?: Request) => Promise<void>);
|
||||
};
|
||||
};
|
||||
|
||||
export type ThirdwebAuthContext<
|
||||
TData extends Json = Json,
|
||||
TSession extends Json = Json,
|
||||
> = Omit<Omit<ThirdwebAuthConfig<TData, TSession>, "wallet">, "domain"> & {
|
||||
auth: ThirdwebAuth;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./core";
|
||||
@@ -1,145 +0,0 @@
|
||||
import { ThirdwebNextAuthConfig } from "./types";
|
||||
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
|
||||
import { serialize } from "cookie";
|
||||
import {
|
||||
GetServerSidePropsContext,
|
||||
NextApiRequest,
|
||||
NextApiResponse,
|
||||
} from "next";
|
||||
import NextAuth, {
|
||||
NextAuthOptions,
|
||||
Session,
|
||||
unstable_getServerSession,
|
||||
} from "next-auth";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
|
||||
export function ThirdwebNextAuth(cfg: ThirdwebNextAuthConfig) {
|
||||
const sdk = ThirdwebSDK.fromPrivateKey(cfg.privateKey, "mainnet");
|
||||
|
||||
function ThirdwebProvider(res: GetServerSidePropsContext["res"]) {
|
||||
return CredentialsProvider({
|
||||
name: "ThirdwebAuth",
|
||||
credentials: {
|
||||
payload: {
|
||||
label: "Payload",
|
||||
type: "text",
|
||||
placeholder: "",
|
||||
},
|
||||
},
|
||||
async authorize({ payload }: any) {
|
||||
try {
|
||||
const parsed = JSON.parse(payload);
|
||||
const token = await sdk.auth.generateAuthToken(cfg.domain, parsed);
|
||||
const address = await sdk.auth.authenticate(cfg.domain, token);
|
||||
|
||||
// Securely set httpOnly cookie on request to prevent XSS on frontend
|
||||
// And set path to / to enable thirdweb_auth_token usage on all endpoints
|
||||
res.setHeader(
|
||||
"Set-Cookie",
|
||||
serialize("thirdweb_auth_token", token, {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "none",
|
||||
}),
|
||||
);
|
||||
|
||||
return { id: address, address };
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function nextOptions(
|
||||
req: GetServerSidePropsContext["req"],
|
||||
res: GetServerSidePropsContext["res"],
|
||||
): NextAuthOptions {
|
||||
async function session({
|
||||
session: _session,
|
||||
}: {
|
||||
session: Session;
|
||||
}): Promise<Session> {
|
||||
const token = req.cookies.thirdweb_auth_token || "";
|
||||
try {
|
||||
const address = await sdk.auth.authenticate(cfg.domain, token);
|
||||
_session.user = { ..._session.user, address } as Session["user"];
|
||||
return _session;
|
||||
} catch {
|
||||
return _session;
|
||||
}
|
||||
}
|
||||
|
||||
function signOut() {
|
||||
res.setHeader(
|
||||
"Set-Cookie",
|
||||
serialize("thirdweb_auth_token", "", {
|
||||
path: "/",
|
||||
expires: new Date(Date.now() + 5 * 1000),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const providers: NextAuthOptions["providers"] = [
|
||||
...cfg.nextOptions.providers,
|
||||
ThirdwebProvider(res),
|
||||
];
|
||||
|
||||
const configSession = cfg.nextOptions.callbacks?.session;
|
||||
const callbacks: NextAuthOptions["callbacks"] = {
|
||||
...cfg.nextOptions.callbacks,
|
||||
session: configSession
|
||||
? async (params) => {
|
||||
params.session = await session(params);
|
||||
return configSession(params);
|
||||
}
|
||||
: session,
|
||||
};
|
||||
|
||||
const configSignOut = cfg.nextOptions.events?.signOut;
|
||||
const events: NextAuthOptions["events"] = {
|
||||
...cfg.nextOptions.events,
|
||||
signOut: configSignOut
|
||||
? async (params) => {
|
||||
signOut();
|
||||
return configSignOut(params);
|
||||
}
|
||||
: signOut,
|
||||
};
|
||||
|
||||
return {
|
||||
...cfg.nextOptions,
|
||||
providers,
|
||||
callbacks,
|
||||
events,
|
||||
};
|
||||
}
|
||||
|
||||
async function getUser(
|
||||
...args:
|
||||
| [NextApiRequest, NextApiResponse]
|
||||
| [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]]
|
||||
) {
|
||||
return unstable_getServerSession(
|
||||
args[0],
|
||||
args[1],
|
||||
nextOptions(args[0], args[1]),
|
||||
);
|
||||
}
|
||||
|
||||
function NextAuthHandler(...args: [] | [NextApiRequest, NextApiResponse]) {
|
||||
if (args.length === 0) {
|
||||
return (req: NextApiRequest, res: NextApiResponse) => {
|
||||
return NextAuth(req, res, nextOptions(req, res));
|
||||
};
|
||||
}
|
||||
|
||||
return NextAuth(args[0], args[1], nextOptions(args[0], args[1]));
|
||||
}
|
||||
|
||||
return {
|
||||
NextAuthHandler,
|
||||
getUser,
|
||||
};
|
||||
}
|
||||
@@ -1 +1,145 @@
|
||||
export * from "./evm";
|
||||
import { ThirdwebAuth } from "../core";
|
||||
import { ThirdwebNextAuthConfig } from "./types";
|
||||
import { serialize } from "cookie";
|
||||
import {
|
||||
GetServerSidePropsContext,
|
||||
NextApiRequest,
|
||||
NextApiResponse,
|
||||
} from "next";
|
||||
import NextAuth, {
|
||||
NextAuthOptions,
|
||||
Session,
|
||||
unstable_getServerSession,
|
||||
} from "next-auth";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
|
||||
export function ThirdwebNextAuth(cfg: ThirdwebNextAuthConfig) {
|
||||
const auth = new ThirdwebAuth(cfg.wallet, cfg.domain);
|
||||
|
||||
function ThirdwebProvider(res: GetServerSidePropsContext["res"]) {
|
||||
return CredentialsProvider({
|
||||
name: "ThirdwebAuth",
|
||||
credentials: {
|
||||
payload: {
|
||||
label: "Payload",
|
||||
type: "text",
|
||||
placeholder: "",
|
||||
},
|
||||
},
|
||||
async authorize({ payload }: any) {
|
||||
try {
|
||||
const parsed = JSON.parse(payload);
|
||||
const token = await auth.generate(parsed);
|
||||
const user = await auth.authenticate(token);
|
||||
|
||||
// Securely set httpOnly cookie on request to prevent XSS on frontend
|
||||
// And set path to / to enable thirdweb_auth_token usage on all endpoints
|
||||
res.setHeader(
|
||||
"Set-Cookie",
|
||||
serialize("thirdweb_auth_token", token, {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "none",
|
||||
}),
|
||||
);
|
||||
|
||||
return { id: user.address, address: user.address };
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function nextOptions(
|
||||
req: GetServerSidePropsContext["req"],
|
||||
res: GetServerSidePropsContext["res"],
|
||||
): NextAuthOptions {
|
||||
async function session({
|
||||
session: _session,
|
||||
}: {
|
||||
session: Session;
|
||||
}): Promise<Session> {
|
||||
const token = req.cookies.thirdweb_auth_token || "";
|
||||
try {
|
||||
const address = await auth.authenticate(token);
|
||||
_session.user = { ..._session.user, address } as Session["user"];
|
||||
return _session;
|
||||
} catch {
|
||||
return _session;
|
||||
}
|
||||
}
|
||||
|
||||
function signOut() {
|
||||
res.setHeader(
|
||||
"Set-Cookie",
|
||||
serialize("thirdweb_auth_token", "", {
|
||||
path: "/",
|
||||
expires: new Date(Date.now() + 5 * 1000),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const providers: NextAuthOptions["providers"] = [
|
||||
...cfg.nextOptions.providers,
|
||||
ThirdwebProvider(res),
|
||||
];
|
||||
|
||||
const configSession = cfg.nextOptions.callbacks?.session;
|
||||
const callbacks: NextAuthOptions["callbacks"] = {
|
||||
...cfg.nextOptions.callbacks,
|
||||
session: configSession
|
||||
? async (params) => {
|
||||
params.session = await session(params);
|
||||
return configSession(params);
|
||||
}
|
||||
: session,
|
||||
};
|
||||
|
||||
const configSignOut = cfg.nextOptions.events?.signOut;
|
||||
const events: NextAuthOptions["events"] = {
|
||||
...cfg.nextOptions.events,
|
||||
signOut: configSignOut
|
||||
? async (params) => {
|
||||
signOut();
|
||||
return configSignOut(params);
|
||||
}
|
||||
: signOut,
|
||||
};
|
||||
|
||||
return {
|
||||
...cfg.nextOptions,
|
||||
providers,
|
||||
callbacks,
|
||||
events,
|
||||
};
|
||||
}
|
||||
|
||||
async function getUser(
|
||||
...args:
|
||||
| [NextApiRequest, NextApiResponse]
|
||||
| [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]]
|
||||
) {
|
||||
return unstable_getServerSession(
|
||||
args[0],
|
||||
args[1],
|
||||
nextOptions(args[0], args[1]),
|
||||
);
|
||||
}
|
||||
|
||||
function NextAuthHandler(...args: [] | [NextApiRequest, NextApiResponse]) {
|
||||
if (args.length === 0) {
|
||||
return (req: NextApiRequest, res: NextApiResponse) => {
|
||||
return NextAuth(req, res, nextOptions(req, res));
|
||||
};
|
||||
}
|
||||
|
||||
return NextAuth(args[0], args[1], nextOptions(args[0], args[1]));
|
||||
}
|
||||
|
||||
return {
|
||||
NextAuthHandler,
|
||||
getUser,
|
||||
};
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { GenericAuthWallet } from "@thirdweb-dev/wallets";
|
||||
import { NextAuthOptions } from "next-auth";
|
||||
|
||||
export type ThirdwebNextAuthConfig = {
|
||||
privateKey: string;
|
||||
domain: string;
|
||||
wallet: GenericAuthWallet;
|
||||
nextOptions: NextAuthOptions;
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
import loginHandler from "./routes/login";
|
||||
import logoutHandler from "./routes/logout";
|
||||
import userHandler from "./routes/user";
|
||||
import {
|
||||
ThirdwebAuthConfig,
|
||||
ThirdwebAuthContext,
|
||||
ThirdwebAuthRoute,
|
||||
ThirdwebAuthUser,
|
||||
} from "./types";
|
||||
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
|
||||
import { NextRequest } from "next/server";
|
||||
import {
|
||||
GetServerSidePropsContext,
|
||||
NextApiRequest,
|
||||
NextApiResponse,
|
||||
} from "next/types";
|
||||
|
||||
export * from "./types";
|
||||
|
||||
async function ThirdwebAuthRouter(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
ctx: ThirdwebAuthContext,
|
||||
) {
|
||||
// Catch-all route must be named with [...thirdweb]
|
||||
const { thirdweb } = req.query;
|
||||
const action = thirdweb?.[0] as ThirdwebAuthRoute;
|
||||
|
||||
switch (action) {
|
||||
case "login":
|
||||
return await loginHandler(req, res, ctx);
|
||||
case "user":
|
||||
return await userHandler(req, res, ctx);
|
||||
case "logout":
|
||||
return await logoutHandler(req, res);
|
||||
default:
|
||||
return res.status(400).json({
|
||||
message: "Invalid route for authentication.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function ThirdwebAuth(cfg: ThirdwebAuthConfig) {
|
||||
const ctx = {
|
||||
...cfg,
|
||||
sdk: ThirdwebSDK.fromPrivateKey(cfg.privateKey, "mainnet"),
|
||||
};
|
||||
|
||||
function ThirdwebAuthHandler(
|
||||
...args: [] | [NextApiRequest, NextApiResponse]
|
||||
) {
|
||||
if (args.length === 0) {
|
||||
return async (req: NextApiRequest, res: NextApiResponse) =>
|
||||
await ThirdwebAuthRouter(req, res, ctx);
|
||||
}
|
||||
|
||||
return ThirdwebAuthRouter(args[0], args[1], ctx);
|
||||
}
|
||||
|
||||
async function getUser(
|
||||
req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest,
|
||||
) {
|
||||
const { sdk, domain } = ctx;
|
||||
let user: ThirdwebAuthUser | null = null;
|
||||
const token =
|
||||
typeof req.cookies.get === "function"
|
||||
? (req.cookies as any).get("thirdweb_auth_token")
|
||||
: (req.cookies as any).thirdweb_auth_token;
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
const address = await sdk.auth.authenticate(domain, token);
|
||||
|
||||
let data = {};
|
||||
if (ctx.callbacks?.user) {
|
||||
data = await ctx.callbacks.user(address);
|
||||
}
|
||||
|
||||
user = { ...data, address };
|
||||
} catch {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
return { ThirdwebAuthHandler, getUser };
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { ThirdwebAuthContext } from "../types";
|
||||
import { LoginPayload } from "@thirdweb-dev/sdk";
|
||||
import { serialize } from "cookie";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
function redirectWithError(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
error: string,
|
||||
) {
|
||||
const encodedError = encodeURIComponent(error);
|
||||
const url = new URL(req.headers.referer as string);
|
||||
url.searchParams.set("error", encodedError);
|
||||
return res.redirect(url.toString());
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
ctx: ThirdwebAuthContext,
|
||||
) {
|
||||
if (req.method !== "GET") {
|
||||
return redirectWithError(req, res, "INVALID_METHOD");
|
||||
}
|
||||
|
||||
const { sdk, domain } = ctx;
|
||||
|
||||
// Get signed login payload from the frontend
|
||||
const payload = JSON.parse(atob(req.query.payload as string)) as LoginPayload;
|
||||
if (!payload) {
|
||||
redirectWithError(req, res, "MISSING_LOGIN_PAYLOAD");
|
||||
}
|
||||
|
||||
let token;
|
||||
try {
|
||||
// Generate an access token with the SDK using the signed payload
|
||||
token = await sdk.auth.generateAuthToken(domain, payload);
|
||||
} catch {
|
||||
return redirectWithError(req, res, "INVALID_LOGIN_PAYLOAD");
|
||||
}
|
||||
|
||||
// Securely set httpOnly cookie on request to prevent XSS on frontend
|
||||
// And set path to / to enable thirdweb_auth_token usage on all endpoints
|
||||
res.setHeader(
|
||||
"Set-Cookie",
|
||||
serialize("thirdweb_auth_token", token, {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "none",
|
||||
}),
|
||||
);
|
||||
|
||||
if (ctx.callbacks?.login) {
|
||||
const address = sdk.auth.verify(domain, payload);
|
||||
await ctx.callbacks.login(address);
|
||||
}
|
||||
|
||||
return res.status(301).redirect(req.query.redirect as string);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { serialize } from "cookie";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
if (req.method !== "GET") {
|
||||
return res.status(400).json({
|
||||
error: "Invalid method. Only GET supported.",
|
||||
});
|
||||
}
|
||||
|
||||
// Set the access token to 'none' and expire in 5 seconds
|
||||
res.setHeader(
|
||||
"Set-Cookie",
|
||||
serialize("thirdweb_auth_token", "", {
|
||||
path: "/",
|
||||
expires: new Date(Date.now() + 5 * 1000),
|
||||
}),
|
||||
);
|
||||
|
||||
return res.status(301).redirect(req.headers.referer as string);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { ThirdwebAuthContext, ThirdwebAuthUser } from "../types";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
ctx: ThirdwebAuthContext,
|
||||
) {
|
||||
if (req.method !== "GET") {
|
||||
return res.status(400).json({
|
||||
error: "Invalid method. Only GET supported.",
|
||||
});
|
||||
}
|
||||
|
||||
const { sdk, domain } = ctx;
|
||||
let user = null;
|
||||
const token = req.cookies.thirdweb_auth_token;
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
const address = await sdk.auth.authenticate(domain, token);
|
||||
|
||||
if (ctx.callbacks?.user) {
|
||||
user = await ctx.callbacks.user(address);
|
||||
}
|
||||
|
||||
user = { ...user, address };
|
||||
} catch {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).json(user as ThirdwebAuthUser | null);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
|
||||
|
||||
export type ThirdwebAuthRoute = "login" | "logout" | "user";
|
||||
|
||||
export type ThirdwebAuthConfig = {
|
||||
privateKey: string;
|
||||
domain: string;
|
||||
callbacks?: {
|
||||
login?: (address: string) => Promise<void> | void;
|
||||
user?: (
|
||||
address: string,
|
||||
) =>
|
||||
| Promise<Omit<ThirdwebAuthUser, "address">>
|
||||
| Omit<ThirdwebAuthUser, "address">;
|
||||
};
|
||||
};
|
||||
|
||||
export type ThirdwebAuthContext = {
|
||||
sdk: ThirdwebSDK;
|
||||
domain: string;
|
||||
callbacks?: {
|
||||
login?: (address: string) => Promise<void> | void;
|
||||
user?: (
|
||||
address: string,
|
||||
) =>
|
||||
| Promise<Omit<ThirdwebAuthUser, "address">>
|
||||
| Omit<ThirdwebAuthUser, "address">;
|
||||
};
|
||||
};
|
||||
|
||||
export type ThirdwebAuthUser = {
|
||||
address: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Json } from "../../core/schema";
|
||||
import { ThirdwebAuthContext, ThirdwebAuthUser } from "../types";
|
||||
import { GetServerSidePropsContext, NextApiRequest } from "next";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
function getToken(
|
||||
req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest,
|
||||
): string | undefined {
|
||||
if (!!(req as NextApiRequest).headers["authorization"]) {
|
||||
const authorizationHeader = (req as NextApiRequest).headers[
|
||||
"authorization"
|
||||
]?.split(" ");
|
||||
if (authorizationHeader?.length === 2) {
|
||||
return authorizationHeader[1];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cookie: string | undefined = !req.cookies
|
||||
? undefined
|
||||
: typeof req.cookies.get === "function"
|
||||
? (req.cookies as any).get("thirdweb_auth_token")
|
||||
: (req.cookies as any).thirdweb_auth_token;
|
||||
|
||||
return cookie;
|
||||
}
|
||||
|
||||
export async function getUser<
|
||||
TData extends Json = Json,
|
||||
TSession extends Json = Json,
|
||||
>(
|
||||
req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest,
|
||||
ctx: ThirdwebAuthContext<TData, TSession>,
|
||||
): Promise<ThirdwebAuthUser<TData, TSession> | null> {
|
||||
const token = getToken(req);
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let authenticatedUser: ThirdwebAuthUser<TData, TSession>;
|
||||
try {
|
||||
authenticatedUser = await ctx.auth.authenticate<TSession>(token, {
|
||||
validateTokenId: async (tokenId: string) => {
|
||||
if (ctx.authOptions?.validateTokenId) {
|
||||
await ctx.authOptions?.validateTokenId(tokenId);
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!ctx.callbacks?.onUser) {
|
||||
return authenticatedUser;
|
||||
}
|
||||
|
||||
const data = await ctx.callbacks.onUser(authenticatedUser);
|
||||
if (!data) {
|
||||
return authenticatedUser;
|
||||
}
|
||||
|
||||
return { ...authenticatedUser, data: data };
|
||||
}
|
||||
@@ -1 +1,71 @@
|
||||
export * from "./evm";
|
||||
import { Json, ThirdwebAuth as ThirdwebAuthSDK } from "../core";
|
||||
import { getUser } from "./helpers/user";
|
||||
import loginHandler from "./routes/login";
|
||||
import logoutHandler from "./routes/logout";
|
||||
import userHandler from "./routes/user";
|
||||
import {
|
||||
ThirdwebAuthConfig,
|
||||
ThirdwebAuthContext,
|
||||
ThirdwebAuthRoute,
|
||||
} from "./types";
|
||||
import { NextRequest } from "next/server";
|
||||
import {
|
||||
GetServerSidePropsContext,
|
||||
NextApiRequest,
|
||||
NextApiResponse,
|
||||
} from "next/types";
|
||||
|
||||
export * from "./types";
|
||||
|
||||
async function ThirdwebAuthRouter(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
ctx: ThirdwebAuthContext,
|
||||
) {
|
||||
// Catch-all route must be named with [...thirdweb]
|
||||
const { thirdweb } = req.query;
|
||||
const action = thirdweb?.[0] as ThirdwebAuthRoute;
|
||||
|
||||
switch (action) {
|
||||
case "login":
|
||||
return await loginHandler(req, res, ctx);
|
||||
case "user":
|
||||
return await userHandler(req, res, ctx);
|
||||
case "logout":
|
||||
return await logoutHandler(req, res, ctx);
|
||||
default:
|
||||
return res.status(400).json({
|
||||
message: "Invalid route for authentication.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function ThirdwebAuth<
|
||||
TData extends Json = Json,
|
||||
TSession extends Json = Json,
|
||||
>(cfg: ThirdwebAuthConfig<TData, TSession>) {
|
||||
const ctx = {
|
||||
...cfg,
|
||||
auth: new ThirdwebAuthSDK(cfg.wallet, cfg.domain),
|
||||
};
|
||||
|
||||
function ThirdwebAuthHandler(
|
||||
...args: [] | [NextApiRequest, NextApiResponse]
|
||||
) {
|
||||
if (args.length === 0) {
|
||||
return async (req: NextApiRequest, res: NextApiResponse) =>
|
||||
await ThirdwebAuthRouter(req, res, ctx as ThirdwebAuthContext);
|
||||
}
|
||||
|
||||
return ThirdwebAuthRouter(args[0], args[1], ctx as ThirdwebAuthContext);
|
||||
}
|
||||
|
||||
return {
|
||||
ThirdwebAuthHandler,
|
||||
getUser: (
|
||||
req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest,
|
||||
) => {
|
||||
return getUser<TData, TSession>(req, ctx);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { GenerateOptions } from "../../core";
|
||||
import { LoginPayloadBodySchema, ThirdwebAuthContext } from "../types";
|
||||
import { serialize } from "cookie";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
ctx: ThirdwebAuthContext,
|
||||
) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
const parsedPayload = LoginPayloadBodySchema.safeParse(req.body);
|
||||
|
||||
// Get signed login payload from the frontend
|
||||
if (!parsedPayload.success) {
|
||||
return res.status(400).json({ error: "Invalid login payload" });
|
||||
}
|
||||
|
||||
const payload = parsedPayload.data.payload;
|
||||
|
||||
const validateNonce = async (nonce: string) => {
|
||||
if (ctx.authOptions?.validateNonce) {
|
||||
await ctx.authOptions?.validateNonce(nonce);
|
||||
}
|
||||
};
|
||||
|
||||
const getSession = async (address: string) => {
|
||||
if (ctx.callbacks?.onLogin) {
|
||||
return ctx.callbacks.onLogin(address, req);
|
||||
}
|
||||
};
|
||||
|
||||
const expirationTime = ctx.authOptions?.tokenDurationInSeconds
|
||||
? new Date(Date.now() + 1000 * ctx.authOptions.tokenDurationInSeconds)
|
||||
: undefined;
|
||||
|
||||
const generateOptions: GenerateOptions = {
|
||||
verifyOptions: {
|
||||
statement: ctx.authOptions?.statement,
|
||||
uri: ctx.authOptions?.uri,
|
||||
version: ctx.authOptions?.version,
|
||||
chainId: ctx.authOptions?.chainId,
|
||||
validateNonce,
|
||||
resources: ctx.authOptions?.resources,
|
||||
},
|
||||
expirationTime,
|
||||
session: getSession,
|
||||
};
|
||||
|
||||
let token: string;
|
||||
try {
|
||||
// Generate an access token with the SDK using the signed payload
|
||||
token = await ctx.auth.generate(payload, generateOptions);
|
||||
} catch (err: any) {
|
||||
if (err.message) {
|
||||
return res.status(400).json({ error: err.message });
|
||||
} else if (typeof err === "string") {
|
||||
return res.status(400).json({ error: err });
|
||||
} else {
|
||||
return res.status(400).json({ error: "Invalid login payload" });
|
||||
}
|
||||
}
|
||||
|
||||
// Securely set httpOnly cookie on request to prevent XSS on frontend
|
||||
// And set path to / to enable thirdweb_auth_token usage on all endpoints
|
||||
res.setHeader(
|
||||
"Set-Cookie",
|
||||
serialize("thirdweb_auth_token", token, {
|
||||
domain: ctx.cookieOptions?.domain,
|
||||
path: ctx.cookieOptions?.path || "/",
|
||||
sameSite: ctx.cookieOptions?.sameSite || "none",
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
}),
|
||||
);
|
||||
|
||||
// Send token in body and as cookie for frontend and backend use cases
|
||||
return res.status(200).json({ token });
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { getUser } from "../helpers/user";
|
||||
import { ThirdwebAuthContext } from "../types";
|
||||
import { serialize } from "cookie";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
ctx: ThirdwebAuthContext,
|
||||
) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({
|
||||
error: "Invalid method. Only POST supported.",
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.callbacks?.onLogout) {
|
||||
const user = await getUser(req, ctx);
|
||||
if (user) {
|
||||
await ctx.callbacks.onLogout(user, req);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the access token to 'none' and expire in 5 seconds
|
||||
res.setHeader(
|
||||
"Set-Cookie",
|
||||
serialize("thirdweb_auth_token", "", {
|
||||
domain: ctx.cookieOptions?.domain,
|
||||
path: ctx.cookieOptions?.path || "/",
|
||||
expires: new Date(Date.now() + 5 * 1000),
|
||||
}),
|
||||
);
|
||||
|
||||
return res.status(200).json({ message: "Succesfully logged out" });
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { getUser } from "../helpers/user";
|
||||
import { ThirdwebAuthContext } from "../types";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
ctx: ThirdwebAuthContext,
|
||||
) {
|
||||
if (req.method !== "GET") {
|
||||
return res.status(400).json({
|
||||
error: "Invalid method. Only GET supported.",
|
||||
});
|
||||
}
|
||||
|
||||
const user = await getUser(req, ctx);
|
||||
return res.status(200).json(user);
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import loginHandler from "./routes/login";
|
||||
import logoutHandler from "./routes/logout";
|
||||
import userHandler from "./routes/user";
|
||||
import {
|
||||
ThirdwebAuthConfig,
|
||||
ThirdwebAuthContext,
|
||||
ThirdwebAuthRoute,
|
||||
ThirdwebAuthUser,
|
||||
} from "./types";
|
||||
import { ThirdwebSDK } from "@thirdweb-dev/sdk/solana";
|
||||
import { NextRequest } from "next/server";
|
||||
import {
|
||||
GetServerSidePropsContext,
|
||||
NextApiRequest,
|
||||
NextApiResponse,
|
||||
} from "next/types";
|
||||
|
||||
export * from "./types";
|
||||
|
||||
async function ThirdwebAuthRouter(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
ctx: ThirdwebAuthContext,
|
||||
) {
|
||||
// Catch-all route must be named with [...thirdweb]
|
||||
const { thirdweb } = req.query;
|
||||
const action = thirdweb?.[0] as ThirdwebAuthRoute;
|
||||
|
||||
switch (action) {
|
||||
case "login":
|
||||
return await loginHandler(req, res, ctx);
|
||||
case "user":
|
||||
return await userHandler(req, res, ctx);
|
||||
case "logout":
|
||||
return await logoutHandler(req, res);
|
||||
default:
|
||||
return res.status(400).json({
|
||||
message: "Invalid route for authentication.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function ThirdwebAuth(cfg: ThirdwebAuthConfig) {
|
||||
const ctx = {
|
||||
...cfg,
|
||||
sdk: ThirdwebSDK.fromPrivateKey("mainnet-beta", cfg.privateKey),
|
||||
};
|
||||
|
||||
function ThirdwebAuthHandler(
|
||||
...args: [] | [NextApiRequest, NextApiResponse]
|
||||
) {
|
||||
if (args.length === 0) {
|
||||
return async (req: NextApiRequest, res: NextApiResponse) =>
|
||||
await ThirdwebAuthRouter(req, res, ctx);
|
||||
}
|
||||
|
||||
return ThirdwebAuthRouter(args[0], args[1], ctx);
|
||||
}
|
||||
|
||||
async function getUser(
|
||||
req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest,
|
||||
) {
|
||||
const { sdk, domain } = ctx;
|
||||
let user: ThirdwebAuthUser | null = null;
|
||||
const token =
|
||||
typeof req.cookies.get === "function"
|
||||
? (req.cookies as any).get("thirdweb_auth_token")
|
||||
: (req.cookies as any).thirdweb_auth_token;
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
const address = await sdk.auth.authenticate(domain, token);
|
||||
|
||||
let data = {};
|
||||
if (ctx.callbacks?.user) {
|
||||
data = await ctx.callbacks.user(address);
|
||||
}
|
||||
|
||||
user = { ...data, address };
|
||||
} catch {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
return { ThirdwebAuthHandler, getUser };
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { ThirdwebAuthContext } from "../types";
|
||||
import { LoginPayload } from "@thirdweb-dev/sdk";
|
||||
import { serialize } from "cookie";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
function redirectWithError(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
error: string,
|
||||
) {
|
||||
const encodedError = encodeURIComponent(error);
|
||||
const url = new URL(req.headers.referer as string);
|
||||
url.searchParams.set("error", encodedError);
|
||||
return res.redirect(url.toString());
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
ctx: ThirdwebAuthContext,
|
||||
) {
|
||||
if (req.method !== "GET") {
|
||||
return redirectWithError(req, res, "INVALID_METHOD");
|
||||
}
|
||||
|
||||
const { sdk, domain } = ctx;
|
||||
|
||||
// Get signed login payload from the frontend
|
||||
const payload = JSON.parse(atob(req.query.payload as string)) as LoginPayload;
|
||||
if (!payload) {
|
||||
redirectWithError(req, res, "MISSING_LOGIN_PAYLOAD");
|
||||
}
|
||||
|
||||
let token;
|
||||
try {
|
||||
// Generate an access token with the SDK using the signed payload
|
||||
token = await sdk.auth.generateAuthToken(domain, payload);
|
||||
} catch {
|
||||
return redirectWithError(req, res, "INVALID_LOGIN_PAYLOAD");
|
||||
}
|
||||
|
||||
// Securely set httpOnly cookie on request to prevent XSS on frontend
|
||||
// And set path to / to enable thirdweb_auth_token usage on all endpoints
|
||||
res.setHeader(
|
||||
"Set-Cookie",
|
||||
serialize("thirdweb_auth_token", token, {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "none",
|
||||
}),
|
||||
);
|
||||
|
||||
if (ctx.callbacks?.login) {
|
||||
const address = sdk.auth.verify(domain, payload);
|
||||
await ctx.callbacks.login(address);
|
||||
}
|
||||
|
||||
return res.status(301).redirect(req.query.redirect as string);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { serialize } from "cookie";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
if (req.method !== "GET") {
|
||||
return res.status(400).json({
|
||||
error: "Invalid method. Only GET supported.",
|
||||
});
|
||||
}
|
||||
|
||||
// Set the access token to 'none' and expire in 5 seconds
|
||||
res.setHeader(
|
||||
"Set-Cookie",
|
||||
serialize("thirdweb_auth_token", "", {
|
||||
path: "/",
|
||||
expires: new Date(Date.now() + 5 * 1000),
|
||||
}),
|
||||
);
|
||||
|
||||
return res.status(301).redirect(req.headers.referer as string);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { ThirdwebAuthContext, ThirdwebAuthUser } from "../types";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
ctx: ThirdwebAuthContext,
|
||||
) {
|
||||
if (req.method !== "GET") {
|
||||
return res.status(400).json({
|
||||
error: "Invalid method. Only GET supported.",
|
||||
});
|
||||
}
|
||||
|
||||
const { sdk, domain } = ctx;
|
||||
let user = null;
|
||||
const token = req.cookies.thirdweb_auth_token;
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
const address = await sdk.auth.authenticate(domain, token);
|
||||
|
||||
if (ctx.callbacks?.user) {
|
||||
user = await ctx.callbacks.user(address);
|
||||
}
|
||||
|
||||
user = { ...user, address };
|
||||
} catch {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).json(user as ThirdwebAuthUser | null);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { ThirdwebSDK } from "@thirdweb-dev/sdk/solana";
|
||||
|
||||
export type ThirdwebAuthRoute = "login" | "logout" | "user";
|
||||
|
||||
export type ThirdwebAuthConfig = {
|
||||
privateKey: string;
|
||||
domain: string;
|
||||
callbacks?: {
|
||||
login?: (address: string) => Promise<void> | void;
|
||||
user?: (
|
||||
address: string,
|
||||
) =>
|
||||
| Promise<Omit<ThirdwebAuthUser, "address">>
|
||||
| Omit<ThirdwebAuthUser, "address">;
|
||||
};
|
||||
};
|
||||
|
||||
export type ThirdwebAuthContext = {
|
||||
sdk: ThirdwebSDK;
|
||||
domain: string;
|
||||
callbacks?: {
|
||||
login?: (address: string) => Promise<void> | void;
|
||||
user?: (
|
||||
address: string,
|
||||
) =>
|
||||
| Promise<Omit<ThirdwebAuthUser, "address">>
|
||||
| Omit<ThirdwebAuthUser, "address">;
|
||||
};
|
||||
};
|
||||
|
||||
export type ThirdwebAuthUser = {
|
||||
address: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import { ThirdwebAuth } from "../../core";
|
||||
import { Json, LoginPayloadOutputSchema, User } from "../../core/schema";
|
||||
import { GenericAuthWallet } from "@thirdweb-dev/wallets";
|
||||
import { GetServerSidePropsContext, NextApiRequest } from "next";
|
||||
import { NextRequest } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
export const LoginPayloadBodySchema = z.object({
|
||||
payload: LoginPayloadOutputSchema,
|
||||
});
|
||||
|
||||
type RequestType =
|
||||
| GetServerSidePropsContext["req"]
|
||||
| NextRequest
|
||||
| NextApiRequest;
|
||||
|
||||
export type ThirdwebAuthRoute = "login" | "logout" | "user";
|
||||
|
||||
export type ThirdwebAuthUser<
|
||||
TData extends Json = Json,
|
||||
TSession extends Json = Json,
|
||||
> = User<TSession> & {
|
||||
data?: TData;
|
||||
};
|
||||
|
||||
export type ThirdwebAuthConfig<
|
||||
TData extends Json = Json,
|
||||
TSession extends Json = Json,
|
||||
> = {
|
||||
domain: string;
|
||||
wallet: GenericAuthWallet;
|
||||
authOptions?: {
|
||||
statement?: string;
|
||||
uri?: string;
|
||||
version?: string;
|
||||
chainId?: string;
|
||||
resources?: string[];
|
||||
validateNonce?:
|
||||
| ((nonce: string) => void)
|
||||
| ((nonce: string) => Promise<void>);
|
||||
validateTokenId?:
|
||||
| ((tokenId: string) => void)
|
||||
| ((tokenId: string) => Promise<void>);
|
||||
tokenDurationInSeconds?: number;
|
||||
};
|
||||
cookieOptions?: {
|
||||
domain?: string;
|
||||
path?: string;
|
||||
sameSite?: "lax" | "strict" | "none";
|
||||
};
|
||||
callbacks?: {
|
||||
onLogin?:
|
||||
| ((address: string, req?: NextApiRequest) => void | TSession)
|
||||
| ((address: string, req?: NextApiRequest) => Promise<void | TSession>);
|
||||
onUser?:
|
||||
| (<TRequestType extends RequestType = RequestType>(
|
||||
user: User<TSession>,
|
||||
req?: TRequestType,
|
||||
) => void | TData)
|
||||
| (<TRequestType extends RequestType = RequestType>(
|
||||
user: User<TSession>,
|
||||
req?: TRequestType,
|
||||
) => Promise<void | TData>);
|
||||
onLogout?:
|
||||
| ((user: User, req?: NextApiRequest) => void)
|
||||
| ((user: User, req?: NextApiRequest) => Promise<void>);
|
||||
};
|
||||
};
|
||||
|
||||
export type ThirdwebAuthContext<
|
||||
TData extends Json = Json,
|
||||
TSession extends Json = Json,
|
||||
> = Omit<Omit<ThirdwebAuthConfig<TData, TSession>, "wallet">, "domain"> & {
|
||||
auth: ThirdwebAuth;
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import * as ed25519 from "@noble/ed25519";
|
||||
import { Signer, Keypair, PublicKey } from "@solana/web3.js";
|
||||
import type { Ecosystem, GenericAuthWallet } from "@thirdweb-dev/wallets";
|
||||
import bs58 from "bs58";
|
||||
import nacl from "tweetnacl";
|
||||
|
||||
export interface SolanaSigner {
|
||||
publicKey: PublicKey;
|
||||
signMessage(message: Uint8Array): Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
export class SignerWallet implements GenericAuthWallet {
|
||||
type: Ecosystem = "solana";
|
||||
private signer: SolanaSigner;
|
||||
|
||||
constructor(signer: SolanaSigner) {
|
||||
this.signer = signer;
|
||||
}
|
||||
|
||||
public async getAddress(): Promise<string> {
|
||||
return this.signer.publicKey.toBase58();
|
||||
}
|
||||
|
||||
public async signMessage(message: string): Promise<string> {
|
||||
const encodedMessage = new TextEncoder().encode(message);
|
||||
const signedMessage = await this.signer.signMessage(encodedMessage);
|
||||
const signature = bs58.encode(signedMessage);
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
public async verifySignature(
|
||||
message: string,
|
||||
signature: string,
|
||||
address: string,
|
||||
): Promise<boolean> {
|
||||
return nacl.sign.detached.verify(
|
||||
new TextEncoder().encode(message),
|
||||
bs58.decode(signature),
|
||||
bs58.decode(address),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class KeypairSigner implements SolanaSigner {
|
||||
private keypair: Signer;
|
||||
public publicKey: PublicKey;
|
||||
|
||||
constructor(keypair: Signer) {
|
||||
this.keypair = keypair;
|
||||
this.publicKey = keypair.publicKey;
|
||||
}
|
||||
|
||||
public async signMessage(message: Uint8Array): Promise<Uint8Array> {
|
||||
return ed25519.sync.sign(message, this.keypair.secretKey.slice(0, 32));
|
||||
}
|
||||
}
|
||||
|
||||
export class KeypairWallet extends SignerWallet {
|
||||
constructor(keypair: Keypair) {
|
||||
super(new KeypairSigner(keypair));
|
||||
}
|
||||
}
|
||||
|
||||
export class PrivateKeyWallet extends KeypairWallet {
|
||||
constructor(privateKey: string) {
|
||||
super(Keypair.fromSecretKey(bs58.decode(privateKey)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extension": [
|
||||
"ts"
|
||||
],
|
||||
"require": [
|
||||
"@swc-node/register"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
import { ThirdwebAuth } from "../src/core";
|
||||
import { SignerWallet } from "../src/evm";
|
||||
import { expect } from "chai";
|
||||
import { Wallet } from "ethers";
|
||||
|
||||
describe("Wallet Authentication", async () => {
|
||||
let adminWallet: any, signerWallet: any, attackerWallet: any;
|
||||
let auth: ThirdwebAuth;
|
||||
|
||||
before(async () => {
|
||||
const [adminSigner, signerSigner, attackerSigner] = [
|
||||
Wallet.createRandom(),
|
||||
Wallet.createRandom(),
|
||||
Wallet.createRandom(),
|
||||
];
|
||||
|
||||
adminWallet = new SignerWallet(adminSigner);
|
||||
signerWallet = new SignerWallet(signerSigner);
|
||||
attackerWallet = new SignerWallet(attackerSigner);
|
||||
|
||||
auth = new ThirdwebAuth(signerWallet, "thirdweb.com");
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
auth.updateWallet(signerWallet);
|
||||
});
|
||||
|
||||
it("Should verify logged in wallet", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const address = await auth.verify(payload);
|
||||
|
||||
expect(address).to.equal(await signerWallet.getAddress());
|
||||
});
|
||||
|
||||
it("Should verify logged in wallet with chain ID and expiration", async () => {
|
||||
const payload = await auth.login({
|
||||
expirationTime: new Date(Date.now() + 1000 * 60 * 5),
|
||||
chainId: "137",
|
||||
});
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const address = await auth.verify(payload, {
|
||||
chainId: "137",
|
||||
});
|
||||
|
||||
expect(address).to.equal(await signerWallet.getAddress());
|
||||
});
|
||||
|
||||
it("Should verify payload with resources", async () => {
|
||||
const payload = await auth.login({
|
||||
resources: ["https://example.com", "https://test.com"],
|
||||
});
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const address = await auth.verify(payload, {
|
||||
resources: ["https://example.com", "https://test.com"],
|
||||
});
|
||||
|
||||
expect(address).to.equal(await signerWallet.getAddress());
|
||||
});
|
||||
|
||||
it("Should reject payload without necessary resources", async () => {
|
||||
const payload = await auth.login({
|
||||
resources: ["https://example.com"],
|
||||
});
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
try {
|
||||
await auth.verify(payload, {
|
||||
resources: ["https://example.com", "https://test.com"],
|
||||
});
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.equal(
|
||||
"Login request is missing required resources: https://test.com",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("Should verify payload with customized statement", async () => {
|
||||
const payload = await auth.login({
|
||||
statement: "Please sign!",
|
||||
});
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const address = await auth.verify(payload, {
|
||||
statement: "Please sign!",
|
||||
});
|
||||
|
||||
expect(address).to.equal(await signerWallet.getAddress());
|
||||
});
|
||||
|
||||
it("Should reject payload with incorrect statement", async () => {
|
||||
const payload = await auth.login({
|
||||
statement: "Please sign!",
|
||||
});
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
try {
|
||||
await auth.verify(payload, {
|
||||
statement: "Please sign again!",
|
||||
});
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.include(
|
||||
"Expected statement 'Please sign again!' does not match statement on payload",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("Should reject invalid nonce", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
try {
|
||||
await auth.verify(payload, {
|
||||
validateNonce: (nonce: string) => {
|
||||
if (nonce === payload.payload.nonce) {
|
||||
throw new Error();
|
||||
}
|
||||
},
|
||||
});
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.equal("Login request nonce is invalid");
|
||||
}
|
||||
});
|
||||
|
||||
it("Should accept valid nonce", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const address = await auth.verify(payload, {
|
||||
validateNonce: (nonce: string) => {
|
||||
if (nonce !== payload.payload.nonce) {
|
||||
throw new Error();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(address).to.equal(await signerWallet.getAddress());
|
||||
});
|
||||
|
||||
it("Should reject payload with incorrect domain", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
try {
|
||||
await auth.verify(payload, { domain: "test.thirdweb.com" });
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.equal(
|
||||
"Expected domain 'test.thirdweb.com' does not match domain on payload 'thirdweb.com'",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("Should reject expired login payload", async () => {
|
||||
const payload = await auth.login({
|
||||
expirationTime: new Date(Date.now() - 1000 * 60 * 5),
|
||||
});
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
try {
|
||||
await auth.verify(payload);
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.equal("Login request has expired");
|
||||
}
|
||||
});
|
||||
|
||||
it("Should reject payload with incorrect chain ID", async () => {
|
||||
const payload = await auth.login({
|
||||
chainId: "1",
|
||||
});
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
try {
|
||||
await auth.verify(payload, {
|
||||
chainId: "137",
|
||||
});
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.equal(
|
||||
"Expected chain ID '137' does not match chain ID on payload '1'",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("Should reject payload with incorrect signer", async () => {
|
||||
const payload = await auth.login();
|
||||
payload.payload.address = await attackerWallet.getAddress();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
try {
|
||||
await auth.verify(payload);
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.contain("does not match payload address");
|
||||
}
|
||||
});
|
||||
|
||||
it("Should generate valid authentication token", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const token = await auth.generate(payload);
|
||||
const user = await auth.authenticate(token);
|
||||
|
||||
expect(user.address).to.equal(await signerWallet.getAddress());
|
||||
});
|
||||
|
||||
it("Should reject token with incorrect domain", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const token = await auth.generate(payload);
|
||||
|
||||
try {
|
||||
await auth.authenticate(token, { domain: "test.thirdweb.com" });
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.contain(
|
||||
"Expected token to be for the domain 'test.thirdweb.com', but found token with domain 'thirdweb.com'",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("Should reject token before invalid before", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const token = await auth.generate(payload, {
|
||||
invalidBefore: new Date(Date.now() + 1000 * 60 * 5),
|
||||
});
|
||||
|
||||
try {
|
||||
await auth.authenticate(token);
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.contain("This token is invalid before");
|
||||
}
|
||||
});
|
||||
|
||||
it("Should reject expired authentication token", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const token = await auth.generate(payload, {
|
||||
expirationTime: new Date(Date.now() - 1000 * 60 * 5),
|
||||
});
|
||||
|
||||
try {
|
||||
await auth.authenticate(token);
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.contain("This token expired");
|
||||
}
|
||||
});
|
||||
|
||||
it("Should reject if admin address is not connected wallet address", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const token = await auth.generate(payload);
|
||||
|
||||
auth.updateWallet(signerWallet);
|
||||
try {
|
||||
await auth.authenticate(token);
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.contain(
|
||||
`Expected the connected wallet address '${await signerWallet.getAddress()}' to match the token issuer address '${await adminWallet.getAddress()}'`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("Should accept token with valid token ID", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const token = await auth.generate(payload, {
|
||||
tokenId: "test",
|
||||
});
|
||||
|
||||
const user = await auth.authenticate(token, {
|
||||
validateTokenId: (tokenId: string) => {
|
||||
if (tokenId !== "test") {
|
||||
throw new Error();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(user.address).to.equal(await signerWallet.getAddress());
|
||||
});
|
||||
|
||||
it("Should reject token with invalid token ID", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const token = await auth.generate(payload, {
|
||||
tokenId: "test",
|
||||
});
|
||||
|
||||
try {
|
||||
await auth.authenticate(token, {
|
||||
validateTokenId: (tokenId: string) => {
|
||||
if (tokenId !== "invalid") {
|
||||
throw new Error();
|
||||
}
|
||||
},
|
||||
});
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.contain("Token ID is invalid");
|
||||
}
|
||||
});
|
||||
|
||||
it("Should propagate session on token", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const token = await auth.generate(payload, {
|
||||
session: { role: "admin" },
|
||||
});
|
||||
|
||||
const user = await auth.authenticate(token);
|
||||
|
||||
expect(user.address).to.equal(await signerWallet.getAddress());
|
||||
expect(user.session).to.deep.equal({ role: "admin" });
|
||||
});
|
||||
|
||||
it("Should call session callback function", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const token = await auth.generate(payload, {
|
||||
session: (address: string) => {
|
||||
return { address, role: "admin" };
|
||||
},
|
||||
});
|
||||
|
||||
const user = await auth.authenticate(token);
|
||||
|
||||
expect(user.address).to.equal(await signerWallet.getAddress());
|
||||
expect(user.session).to.deep.equal({
|
||||
address: await signerWallet.getAddress(),
|
||||
role: "admin",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,353 @@
|
||||
import { ThirdwebAuth } from "../src/core";
|
||||
import { KeypairWallet } from "../src/solana";
|
||||
import { Keypair } from "@solana/web3.js";
|
||||
import { expect } from "chai";
|
||||
|
||||
describe("Wallet Authentication", async () => {
|
||||
let adminWallet: any, signerWallet: any, attackerWallet: any;
|
||||
let auth: ThirdwebAuth;
|
||||
|
||||
before(async () => {
|
||||
const [adminSigner, signerSigner, attackerSigner] = [
|
||||
Keypair.generate(),
|
||||
Keypair.generate(),
|
||||
Keypair.generate(),
|
||||
];
|
||||
|
||||
adminWallet = new KeypairWallet(adminSigner);
|
||||
signerWallet = new KeypairWallet(signerSigner);
|
||||
attackerWallet = new KeypairWallet(attackerSigner);
|
||||
|
||||
auth = new ThirdwebAuth(signerWallet, "thirdweb.com");
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
auth.updateWallet(signerWallet);
|
||||
});
|
||||
|
||||
it("Should verify logged in wallet", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const address = await auth.verify(payload);
|
||||
|
||||
expect(address).to.equal(await signerWallet.getAddress());
|
||||
});
|
||||
|
||||
it("Should verify logged in wallet with chain ID and expiration", async () => {
|
||||
const payload = await auth.login({
|
||||
expirationTime: new Date(Date.now() + 1000 * 60 * 5),
|
||||
chainId: "137",
|
||||
});
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const address = await auth.verify(payload, {
|
||||
chainId: "137",
|
||||
});
|
||||
|
||||
expect(address).to.equal(await signerWallet.getAddress());
|
||||
});
|
||||
|
||||
it("Should verify payload with resources", async () => {
|
||||
const payload = await auth.login({
|
||||
resources: ["https://example.com", "https://test.com"],
|
||||
});
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const address = await auth.verify(payload, {
|
||||
resources: ["https://example.com", "https://test.com"],
|
||||
});
|
||||
|
||||
expect(address).to.equal(await signerWallet.getAddress());
|
||||
});
|
||||
|
||||
it("Should reject payload without necessary resources", async () => {
|
||||
const payload = await auth.login({
|
||||
resources: ["https://example.com"],
|
||||
});
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
try {
|
||||
await auth.verify(payload, {
|
||||
resources: ["https://example.com", "https://test.com"],
|
||||
});
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.equal(
|
||||
"Login request is missing required resources: https://test.com",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("Should verify payload with customized statement", async () => {
|
||||
const payload = await auth.login({
|
||||
statement: "Please sign!",
|
||||
});
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const address = await auth.verify(payload, {
|
||||
statement: "Please sign!",
|
||||
});
|
||||
|
||||
expect(address).to.equal(await signerWallet.getAddress());
|
||||
});
|
||||
|
||||
it("Should reject payload with incorrect statement", async () => {
|
||||
const payload = await auth.login({
|
||||
statement: "Please sign!",
|
||||
});
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
try {
|
||||
await auth.verify(payload, {
|
||||
statement: "Please sign again!",
|
||||
});
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.include(
|
||||
"Expected statement 'Please sign again!' does not match statement on payload",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("Should reject invalid nonce", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
try {
|
||||
await auth.verify(payload, {
|
||||
validateNonce: (nonce: string) => {
|
||||
if (nonce === payload.payload.nonce) {
|
||||
throw new Error();
|
||||
}
|
||||
},
|
||||
});
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.equal("Login request nonce is invalid");
|
||||
}
|
||||
});
|
||||
|
||||
it("Should accept valid nonce", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const address = await auth.verify(payload, {
|
||||
validateNonce: (nonce: string) => {
|
||||
if (nonce !== payload.payload.nonce) {
|
||||
throw new Error();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(address).to.equal(await signerWallet.getAddress());
|
||||
});
|
||||
|
||||
it("Should reject payload with incorrect domain", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
try {
|
||||
await auth.verify(payload, { domain: "test.thirdweb.com" });
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.equal(
|
||||
"Expected domain 'test.thirdweb.com' does not match domain on payload 'thirdweb.com'",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("Should reject expired login payload", async () => {
|
||||
const payload = await auth.login({
|
||||
expirationTime: new Date(Date.now() - 1000 * 60 * 5),
|
||||
});
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
try {
|
||||
await auth.verify(payload);
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.equal("Login request has expired");
|
||||
}
|
||||
});
|
||||
|
||||
it("Should reject payload with incorrect chain ID", async () => {
|
||||
const payload = await auth.login({
|
||||
chainId: "1",
|
||||
});
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
try {
|
||||
await auth.verify(payload, {
|
||||
chainId: "137",
|
||||
});
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.equal(
|
||||
"Expected chain ID '137' does not match chain ID on payload '1'",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("Should reject payload with incorrect signer", async () => {
|
||||
const payload = await auth.login();
|
||||
payload.payload.address = await attackerWallet.getAddress();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
try {
|
||||
await auth.verify(payload);
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.contain("does not match payload address");
|
||||
}
|
||||
});
|
||||
|
||||
it("Should generate valid authentication token", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const token = await auth.generate(payload);
|
||||
const user = await auth.authenticate(token);
|
||||
|
||||
expect(user.address).to.equal(await signerWallet.getAddress());
|
||||
});
|
||||
|
||||
it("Should reject token with incorrect domain", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const token = await auth.generate(payload);
|
||||
|
||||
try {
|
||||
await auth.authenticate(token, { domain: "test.thirdweb.com" });
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.contain(
|
||||
"Expected token to be for the domain 'test.thirdweb.com', but found token with domain 'thirdweb.com'",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("Should reject token before invalid before", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const token = await auth.generate(payload, {
|
||||
invalidBefore: new Date(Date.now() + 1000 * 60 * 5),
|
||||
});
|
||||
|
||||
try {
|
||||
await auth.authenticate(token);
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.contain("This token is invalid before");
|
||||
}
|
||||
});
|
||||
|
||||
it("Should reject expired authentication token", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const token = await auth.generate(payload, {
|
||||
expirationTime: new Date(Date.now() - 1000 * 60 * 5),
|
||||
});
|
||||
|
||||
try {
|
||||
await auth.authenticate(token);
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.contain("This token expired");
|
||||
}
|
||||
});
|
||||
|
||||
it("Should reject if admin address is not connected wallet address", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const token = await auth.generate(payload);
|
||||
|
||||
auth.updateWallet(signerWallet);
|
||||
try {
|
||||
await auth.authenticate(token);
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.contain(
|
||||
`Expected the connected wallet address '${await signerWallet.getAddress()}' to match the token issuer address '${await adminWallet.getAddress()}'`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("Should accept token with valid token ID", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const token = await auth.generate(payload, {
|
||||
tokenId: "test",
|
||||
});
|
||||
|
||||
const user = await auth.authenticate(token, {
|
||||
validateTokenId: (tokenId: string) => {
|
||||
if (tokenId !== "test") {
|
||||
throw new Error();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(user.address).to.equal(await signerWallet.getAddress());
|
||||
});
|
||||
|
||||
it("Should reject token with invalid token ID", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const token = await auth.generate(payload, {
|
||||
tokenId: "test",
|
||||
});
|
||||
|
||||
try {
|
||||
await auth.authenticate(token, {
|
||||
validateTokenId: (tokenId: string) => {
|
||||
if (tokenId !== "invalid") {
|
||||
throw new Error();
|
||||
}
|
||||
},
|
||||
});
|
||||
expect.fail();
|
||||
} catch (err: any) {
|
||||
expect(err.message).to.contain("Token ID is invalid");
|
||||
}
|
||||
});
|
||||
|
||||
it("Should propagate session on token", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const token = await auth.generate(payload, {
|
||||
session: { role: "admin" },
|
||||
});
|
||||
|
||||
const user = await auth.authenticate(token);
|
||||
|
||||
expect(user.address).to.equal(await signerWallet.getAddress());
|
||||
expect(user.session).to.deep.equal({ role: "admin" });
|
||||
});
|
||||
|
||||
it("Should call session callback function", async () => {
|
||||
const payload = await auth.login();
|
||||
|
||||
auth.updateWallet(adminWallet);
|
||||
const token = await auth.generate(payload, {
|
||||
session: (address: string) => {
|
||||
return { address, role: "admin" };
|
||||
},
|
||||
});
|
||||
|
||||
const user = await auth.authenticate(token);
|
||||
|
||||
expect(user.address).to.equal(await signerWallet.getAddress());
|
||||
expect(user.session).to.deep.equal({
|
||||
address: await signerWallet.getAddress(),
|
||||
role: "admin",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@thirdweb-dev/tsconfig/base.json",
|
||||
"extends": "@thirdweb-dev/tsconfig/sdk.json",
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,71 @@
|
||||
# thirdweb
|
||||
|
||||
## 0.9.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#528](https://github.com/thirdweb-dev/js/pull/528) [`ab1f8bb`](https://github.com/thirdweb-dev/js/commit/ab1f8bb6f658dadbca2cb53decbc893050217407) Thanks [@adam-maj](https://github.com/adam-maj)! - Added --skip-update-check flag to CLI to prevent checking for new versions and automatically updating before running commands. Also, only check for check for updates every 24 hours.
|
||||
|
||||
- Updated dependencies [[`b45d761`](https://github.com/thirdweb-dev/js/commit/b45d761f8f2695b48312ec6eff15e0549e6a8a41), [`5f8ec45`](https://github.com/thirdweb-dev/js/commit/5f8ec45af7c81d075a84846098f38cad90d03fb9), [`04de5b2`](https://github.com/thirdweb-dev/js/commit/04de5b278362573991140c3a496de7451e5c770d)]:
|
||||
- @thirdweb-dev/sdk@3.7.3
|
||||
|
||||
## 0.9.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`19b8e18`](https://github.com/thirdweb-dev/js/commit/19b8e18b6bf68e374c817fefbf2fd29ac5573052)]:
|
||||
- @thirdweb-dev/sdk@3.7.2
|
||||
|
||||
## 0.9.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @thirdweb-dev/sdk@3.7.1
|
||||
|
||||
## 0.9.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#517](https://github.com/thirdweb-dev/js/pull/517) [`9e591b3`](https://github.com/thirdweb-dev/js/commit/9e591b32b78f1300b2014d44d591d297249a1f84) Thanks [@avneesh0612](https://github.com/avneesh0612)! - add express, node.js, and react native templates
|
||||
|
||||
- Updated dependencies [[`f37e86b`](https://github.com/thirdweb-dev/js/commit/f37e86b7d21f7da0df6ab549cb903dc99db10a79), [`f15733f`](https://github.com/thirdweb-dev/js/commit/f15733f34c5985eec5593b3d9196e5528ba96443)]:
|
||||
- @thirdweb-dev/sdk@3.7.0
|
||||
|
||||
## 0.9.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`7cab8d1`](https://github.com/thirdweb-dev/js/commit/7cab8d1679f8d007091aa03adb83add3822a504a)]:
|
||||
- @thirdweb-dev/sdk@3.6.11
|
||||
|
||||
## 0.9.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#483](https://github.com/thirdweb-dev/js/pull/483) [`4e8ff58`](https://github.com/thirdweb-dev/js/commit/4e8ff582084c3d201314e6c5dff5b79f15eb6292) Thanks [@jnsdls](https://github.com/jnsdls)! - `thirdweb create` with usage of `forge` should now function correctly in windows CMD shell
|
||||
|
||||
- Updated dependencies [[`5fba324`](https://github.com/thirdweb-dev/js/commit/5fba324ac17ab02bc8f13d82a232bc5c6970c8e5), [`894cbef`](https://github.com/thirdweb-dev/js/commit/894cbefc6361f23fc528a6c6819f5c71793d46e4), [`557429b`](https://github.com/thirdweb-dev/js/commit/557429b5cfb3af2983ee01cf7d12d41ee0557593), [`73dc026`](https://github.com/thirdweb-dev/js/commit/73dc026ff9d0ac9099bd5a9a9cab8fdbfc0ae723), [`9eaa21d`](https://github.com/thirdweb-dev/js/commit/9eaa21d09ab9c700aea61a2a25f8ca9859d20857), [`68c8e3c`](https://github.com/thirdweb-dev/js/commit/68c8e3c4d79f5d56dc4414241bcca0d88285fcca), [`bff433e`](https://github.com/thirdweb-dev/js/commit/bff433e12150dc029e33a578219c8437a510da99)]:
|
||||
- @thirdweb-dev/sdk@3.6.10
|
||||
|
||||
## 0.9.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`e913e0d`](https://github.com/thirdweb-dev/js/commit/e913e0daa9ece9a6274f7ffa4e66bdcbf32e6ada), [`5862c55`](https://github.com/thirdweb-dev/js/commit/5862c558fb48604b5aca4defd1ccc06fc3536358), [`73883f5`](https://github.com/thirdweb-dev/js/commit/73883f56d0ed0a35ace8b98b96caa782443be22e), [`f7a74a3`](https://github.com/thirdweb-dev/js/commit/f7a74a3b7c489ada5b1ec435632a326334a33c9b)]:
|
||||
- @thirdweb-dev/sdk@3.6.9
|
||||
|
||||
## 0.9.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#454](https://github.com/thirdweb-dev/js/pull/454) [`c673e39`](https://github.com/thirdweb-dev/js/commit/c673e39f23ef082097d73d62910580e8fad400a0) Thanks [@jnsdls](https://github.com/jnsdls)! - upgraded dependencies
|
||||
|
||||
- [#461](https://github.com/thirdweb-dev/js/pull/461) [`af85389`](https://github.com/thirdweb-dev/js/commit/af85389dc07fb163b8f9d70093eb1b691e47561a) Thanks [@nachoiacovino](https://github.com/nachoiacovino)! - Fix deprecated links
|
||||
|
||||
- Updated dependencies [[`c673e39`](https://github.com/thirdweb-dev/js/commit/c673e39f23ef082097d73d62910580e8fad400a0), [`ea95c5f`](https://github.com/thirdweb-dev/js/commit/ea95c5f609e306e333ee0f73f7920503358ca848), [`699a2b1`](https://github.com/thirdweb-dev/js/commit/699a2b16fb991c474ec57db8f178e2601d631f39), [`4cdd0bd`](https://github.com/thirdweb-dev/js/commit/4cdd0bd6348494a256d7c6a2bdf8f7b5c20f6877), [`a8267f9`](https://github.com/thirdweb-dev/js/commit/a8267f912df84c58d3fe3f47b90bd474f73c84ca)]:
|
||||
- @thirdweb-dev/sdk@3.6.8
|
||||
|
||||
## 0.9.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
+12
-3
@@ -62,7 +62,7 @@ Deploying released contracts give deployers access to automatic SDKs to integrat
|
||||
npx thirdweb@latest detect
|
||||
```
|
||||
|
||||
As you're developing your contracts, you may want to implement [Extensions](https://portal.thirdweb.com/extensions) to unlock functionality on the SDKs (ie. nft minting with automatic upload to IPFS) and the dashboard (ie. generated UI to manage permissions). This command will show what extensions were detected on your contract, unlocking the corresponding functionality on the SDKs and dashboard.
|
||||
As you're developing your contracts, you may want to implement [Extensions](https://portal.thirdweb.com/contractkit) to unlock functionality on the SDKs (ie. nft minting with automatic upload to IPFS) and the dashboard (ie. generated UI to manage permissions). This command will show what extensions were detected on your contract, unlocking the corresponding functionality on the SDKs and dashboard.
|
||||
|
||||
---
|
||||
|
||||
@@ -101,8 +101,17 @@ $ npx thirdweb@latest release
|
||||
|
||||
The simplest way to work on the CLI locally is to:
|
||||
|
||||
1. Install the package locally
|
||||
2. Run the `build:watch` command to compile any changes in realtime
|
||||
1. Install the package locally by running
|
||||
|
||||
```bash
|
||||
git clone https://github.com/thirdweb-dev/js.git
|
||||
```
|
||||
2. From the project root run
|
||||
|
||||
```bash
|
||||
yarn && yarn build
|
||||
```
|
||||
3. From inside `js/packages/cli` Run the `build:watch` command to compile any changes in real time
|
||||
|
||||
```bash
|
||||
$ npm install -g ./
|
||||
|
||||
@@ -61,6 +61,9 @@ describe("npx thirdweb detect", () => {
|
||||
|
||||
const lines = detect.getStdout();
|
||||
|
||||
// check that detect excited successfully
|
||||
expect(detect.getExitCode()).toEqual(0);
|
||||
|
||||
// Detected extensions
|
||||
expect(lines.findIndex((line) => line.includes("ERC721"))).toBeGreaterThan(
|
||||
-1,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "thirdweb",
|
||||
"main": "dist/cli/index.js",
|
||||
"version": "0.9.6",
|
||||
"version": "0.9.14",
|
||||
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/cli",
|
||||
"author": "thirdweb eng <[email protected]>",
|
||||
"license": "Apache-2.0",
|
||||
@@ -18,12 +18,12 @@
|
||||
"@types/prompts": "^2.0.14",
|
||||
"@types/rimraf": "^3.0.0",
|
||||
"@types/tar": "^6.1.2",
|
||||
"@types/update-notifier": "^5.1.0",
|
||||
"@types/update-notifier": "^6.0.1",
|
||||
"@types/validate-npm-package-name": "^4.0.0",
|
||||
"hardhat": "^2.9.3",
|
||||
"jest": "^28",
|
||||
"jest": "^29",
|
||||
"prettier": "^2.7.1",
|
||||
"ts-jest": "^28.0.8",
|
||||
"ts-jest": "^29.0.3",
|
||||
"ts-node": "^10.7.0",
|
||||
"tsup": "^6.2.3",
|
||||
"typescript": "^4.7.4"
|
||||
@@ -41,6 +41,7 @@
|
||||
"prompts": "^2.4.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"solc": "0.8.17",
|
||||
"sync-disk-cache": "^2.1.0",
|
||||
"tar": "^6.1.11",
|
||||
"tslog": "^3.3.3",
|
||||
"update-notifier": "^6.0.2",
|
||||
@@ -74,4 +75,4 @@
|
||||
"sourcemap": true,
|
||||
"clean": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+164
-129
@@ -3,6 +3,7 @@ import { detectExtensions } from "../common/feature-detector";
|
||||
import { processProject } from "../common/processor";
|
||||
import { cliVersion, pkg } from "../constants/urls";
|
||||
import { info, logger, spinner } from "../core/helpers/logger";
|
||||
import { CacheEntry } from "../core/types/cache";
|
||||
import { twCreate } from "../create/command";
|
||||
import { deploy } from "../deploy";
|
||||
import { findPackageInstallation } from "../helpers/detect-local-packages";
|
||||
@@ -13,12 +14,14 @@ import { exec, spawn } from "child_process";
|
||||
import { Command } from "commander";
|
||||
import open from "open";
|
||||
import prompts from "prompts";
|
||||
import Cache from "sync-disk-cache";
|
||||
|
||||
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();
|
||||
const cache = new Cache("thirdweb:cli");
|
||||
|
||||
// yes this has to look like this, eliminates whitespace
|
||||
if (!skipIntro) {
|
||||
@@ -37,7 +40,164 @@ const main = async () => {
|
||||
program
|
||||
.name("thirdweb-cli")
|
||||
.description("Official thirdweb command line interface")
|
||||
.version(cliVersion, "-v, --version");
|
||||
.version(cliVersion, "-v, --version")
|
||||
.option("--skip-update-check", "Skip check for auto updates")
|
||||
.hook("preAction", async () => {
|
||||
if (skipIntro || program.opts().skipUpdateCheck) {
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldCheckVersion = true;
|
||||
try {
|
||||
const lastCheckCache: CacheEntry = cache.get("last-version-check");
|
||||
|
||||
if (lastCheckCache.isCached) {
|
||||
const lastVersionCheck = new Date(lastCheckCache.value);
|
||||
// Don't check for updates if already checked within past 24 hours
|
||||
if (
|
||||
new Date().getTime() - lastVersionCheck.getTime() <
|
||||
1000 * 60 * 60 * 24
|
||||
) {
|
||||
shouldCheckVersion = false;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
|
||||
if (!shouldCheckVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// Set cache to prevent checking for updates again for 24 hours
|
||||
cache.set("last-version-check", new Date().toISOString());
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
program
|
||||
.command("create [projectType] [projectPath]")
|
||||
@@ -53,6 +213,9 @@ const main = async () => {
|
||||
.option("--cra", "Initialize as a Create React App project.")
|
||||
.option("--next", "Initialize as a Next.js project.")
|
||||
.option("--vite", "Initialize as a Vite project.")
|
||||
.option("--reactNative", "Initialize as a React Native project.")
|
||||
.option("--express", "Initialize as a Express project.")
|
||||
.option("--node", "Initialize as a Node project.")
|
||||
.option(
|
||||
"--use-npm",
|
||||
"Explicitly tell the CLI to bootstrap the app using npm",
|
||||
@@ -198,134 +361,6 @@ const main = async () => {
|
||||
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();
|
||||
};
|
||||
|
||||
|
||||
@@ -72,13 +72,13 @@ export async function detectExtensions(options: any) {
|
||||
const enabledFeatures: Feature[] = features.enabledFeatures.map(
|
||||
(feature) => ({
|
||||
name: feature.name,
|
||||
reference: `https://portal.thirdweb.com/extensions/${feature.name.toLowerCase()}`,
|
||||
reference: `https://portal.thirdweb.com/interfaces/${feature.name.toLowerCase()}`,
|
||||
}),
|
||||
);
|
||||
const suggestedFeatures: Feature[] = features.suggestedFeatures.map(
|
||||
(feature) => ({
|
||||
name: feature.name,
|
||||
reference: `https://portal.thirdweb.com/extensions/${feature.name.toLowerCase()}`,
|
||||
reference: `https://portal.thirdweb.com/interfaces/${feature.name.toLowerCase()}`,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -39,9 +39,9 @@ export async function processProject(
|
||||
if (projectType === "none") {
|
||||
if (command === "deploy") {
|
||||
info(
|
||||
"No contracts detected in this directory. Redirecting to the thirdweb contract deployment page.",
|
||||
"No contracts detected in this directory. Redirecting to the thirdweb contract explore page.",
|
||||
);
|
||||
return "https://thirdweb.com/contracts";
|
||||
return "https://thirdweb.com/explore";
|
||||
}
|
||||
|
||||
error("No detected contracts in this directory.");
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export type CacheEntry = {
|
||||
isCached: boolean;
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
declare module 'sync-disk-cache';
|
||||
declare module "solc";
|
||||
-1
@@ -1 +0,0 @@
|
||||
declare module "solc";
|
||||
@@ -61,6 +61,18 @@ export async function twCreate(
|
||||
framework = "vite";
|
||||
}
|
||||
|
||||
if (options.node) {
|
||||
framework = "node";
|
||||
}
|
||||
|
||||
if (options.express) {
|
||||
framework = "express";
|
||||
}
|
||||
|
||||
if (options.reactNative) {
|
||||
framework = "react-native";
|
||||
}
|
||||
|
||||
if (options.solana) {
|
||||
chain = "solana";
|
||||
}
|
||||
@@ -205,17 +217,22 @@ export async function twCreate(
|
||||
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: "Node.js", value: "node" },
|
||||
{ title: "Express", value: "express" },
|
||||
// Solana doesn't support Vite just yet:
|
||||
// { title: "Vite", value: "vite" },
|
||||
]
|
||||
: [
|
||||
{ title: "Next.js", value: "next" },
|
||||
{ title: "Create React App", value: "cra" },
|
||||
{ title: "Vite", value: "vite" },
|
||||
{ title: "React Native", value: "react-native" },
|
||||
{ title: "Node.js", value: "node" },
|
||||
{ title: "Express", value: "express" },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { runCommand } from "./run-command";
|
||||
import { rm, writeFile } from "node:fs/promises";
|
||||
|
||||
/**
|
||||
* Spawn a package manager installation with either Yarn or NPM.
|
||||
@@ -8,12 +9,12 @@ import { runCommand } from "./run-command";
|
||||
*/
|
||||
export async function submodules(): Promise<void> {
|
||||
// Remove script folder because it clashes with contract templates (and not needed with deploy)
|
||||
await runCommand("rm", ["-rf", "script"]);
|
||||
await rm("script", { recursive: true, force: true });
|
||||
|
||||
// Reset and recursively add forge submodules
|
||||
await runCommand("rm", ["-rf", ".gitmodules"]);
|
||||
await runCommand("rm", ["-rf", "lib"]);
|
||||
await runCommand("touch", [".gitmodules"]);
|
||||
await rm(".gitmodules", { recursive: true, force: true });
|
||||
await rm("lib", { recursive: true, force: true });
|
||||
await writeFile(".gitmodules", "");
|
||||
await runCommand("git", [
|
||||
"submodule",
|
||||
"add",
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @thirdweb-dev/contracts-js
|
||||
|
||||
## 1.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#463](https://github.com/thirdweb-dev/js/pull/463) [`557429b`](https://github.com/thirdweb-dev/js/commit/557429b5cfb3af2983ee01cf7d12d41ee0557593) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - PackVRF contract
|
||||
|
||||
- [#311](https://github.com/thirdweb-dev/js/pull/311) [`9eaa21d`](https://github.com/thirdweb-dev/js/commit/9eaa21d09ab9c700aea61a2a25f8ca9859d20857) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Update to v3.3.0 of contracts package
|
||||
|
||||
- [#311](https://github.com/thirdweb-dev/js/pull/311) [`9eaa21d`](https://github.com/thirdweb-dev/js/commit/9eaa21d09ab9c700aea61a2a25f8ca9859d20857) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Update to 3.1.10 of contracts package
|
||||
|
||||
## 1.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"main": "dist/thirdweb-dev-contracts-js-factories-TWMultichainRegistryLogic__factory.cjs.js",
|
||||
"module": "dist/thirdweb-dev-contracts-js-factories-TWMultichainRegistryLogic__factory.esm.js"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"main": "dist/thirdweb-dev-contracts-js-factories-TWMultichainRegistryRouter__factory.cjs.js",
|
||||
"module": "dist/thirdweb-dev-contracts-js-factories-TWMultichainRegistryRouter__factory.esm.js"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"main": "dist/thirdweb-dev-contracts-js-factories-TWMultichainRegistryStorage__factory.cjs.js",
|
||||
"module": "dist/thirdweb-dev-contracts-js-factories-TWMultichainRegistryStorage__factory.esm.js"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"main": "dist/thirdweb-dev-contracts-js-factories-TWRegistryLegacy__factory.cjs.js",
|
||||
"module": "dist/thirdweb-dev-contracts-js-factories-TWRegistryLegacy__factory.esm.js"
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/contracts-js",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"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.8-0"
|
||||
"@thirdweb-dev/contracts": "3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.18.10",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-typescript",
|
||||
["@babel/preset-react", { "runtime": "automatic" }],
|
||||
["@babel/preset-env", { "targets": "defaults, not ie 11" }]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# folders
|
||||
dist/
|
||||
|
||||
# files
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["thirdweb"],
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
etc/
|
||||
temp/
|
||||
temp-evm/
|
||||
temp-solana/
|
||||
docs/
|
||||
@@ -0,0 +1,82 @@
|
||||
# @thirdweb-dev/react-core
|
||||
|
||||
## 3.7.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`dee4596`](https://github.com/thirdweb-dev/js/commit/dee45965496d5d0298944031dd13a4345f9e1683)]:
|
||||
- @thirdweb-dev/auth@3.0.3
|
||||
|
||||
## 3.7.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @thirdweb-dev/auth@3.0.2
|
||||
|
||||
## 3.7.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#520](https://github.com/thirdweb-dev/js/pull/520) [`8c81ca5`](https://github.com/thirdweb-dev/js/commit/8c81ca5c3033b04b1f64e3a1134a72e7e3ec03b1) Thanks [@adam-maj](https://github.com/adam-maj)! - Update auth and react-core dependencies
|
||||
|
||||
- Updated dependencies [[`8c81ca5`](https://github.com/thirdweb-dev/js/commit/8c81ca5c3033b04b1f64e3a1134a72e7e3ec03b1)]:
|
||||
- @thirdweb-dev/auth@3.0.1
|
||||
|
||||
## 3.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#460](https://github.com/thirdweb-dev/js/pull/460) [`a6c074c`](https://github.com/thirdweb-dev/js/commit/a6c074c3f33148cd17f5a66a58df9272a4381bab) Thanks [@adam-maj](https://github.com/adam-maj)! - All Auth hooks and configuration have been upgraded along with the major upgrade to Auth. This includes changes in necessary `authConfig` to the `ThirdwebProvider`, as well as usage of the `useLogin`, `useLogout`, and `useUser` hooks.
|
||||
|
||||
## How to Upgrade
|
||||
|
||||
In order to upgrade your frontend setup to account for these changes, you'll need to make the following changes to your app:
|
||||
|
||||
**1. Remove `loginRedirect` from `authConfig`**
|
||||
|
||||
In your `ThirdwebProvider`, you can remove the `loginRedirect` option from the `authConfig` object, as the `login` endpoint no longer uses redirects.
|
||||
|
||||
```jsx
|
||||
export default function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
<ThirdwebProvider
|
||||
authConfig={{
|
||||
domain: "example.com",
|
||||
authUrl: "/api/auth",
|
||||
// No more loginRedirect
|
||||
}}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**2. Update `useLogin` and `useLogout` to use object destructuring**
|
||||
|
||||
The `useLogin` and `useLogout` hooks now return an object with a `login` and `logout` function (as well as `isLoading` states), respectively. You'll need to update your usage of these hooks to use object destructuring.
|
||||
|
||||
```jsx
|
||||
import { useLogin, useLogout } from "@thirdweb-dev/react-core";
|
||||
|
||||
export default function Component() {
|
||||
const { login } = useLogin();
|
||||
const { logout } = useLogout();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={login}>Login</button>
|
||||
<button onClick={logout}>Logout</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [#511](https://github.com/thirdweb-dev/js/pull/511) [`62b7388`](https://github.com/thirdweb-dev/js/commit/62b7388bb2f2564fff0c5e86f0a468db65992b4e) Thanks [@ikethirdweb](https://github.com/ikethirdweb)! - react-core init
|
||||
|
||||
`react-core` package creation. The goal is to share as much code as possible between our react and future react-native packages.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#514](https://github.com/thirdweb-dev/js/pull/514) [`48893c7`](https://github.com/thirdweb-dev/js/commit/48893c730e565c962d117b1eca579e240dc6a5ec) Thanks [@jnsdls](https://github.com/jnsdls)! - switch to using the new `@thirdweb-dev/react-core` package to power `@thirdweb-dev/react`
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2021 Non-Fungible Labs, Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,18 @@
|
||||
<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 React Core SDK</h1>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/@thirdweb-dev/react"><img src="https://img.shields.io/npm/v/@thirdweb-dev/react?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>Proudly powering react and react-native.</strong></p>
|
||||
<br />
|
||||
|
||||
<br />
|
||||
|
||||
You are likely looking for [@thirdweb-dev/react](https://www.npmjs.com/package/@thirdweb-dev/react).
|
||||
@@ -0,0 +1,364 @@
|
||||
/**
|
||||
* Config file for API Extractor. For more info, please visit: https://api-extractor.com
|
||||
*/
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
|
||||
|
||||
/**
|
||||
* Optionally specifies another JSON config file that this file extends from. This provides a way for
|
||||
* standard settings to be shared across multiple projects.
|
||||
*
|
||||
* If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains
|
||||
* the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be
|
||||
* resolved using NodeJS require().
|
||||
*
|
||||
* SUPPORTED TOKENS: none
|
||||
* DEFAULT VALUE: ""
|
||||
*/
|
||||
// "extends": "./shared/api-extractor-base.json"
|
||||
// "extends": "my-package/include/api-extractor-base.json"
|
||||
|
||||
/**
|
||||
* Determines the "<projectFolder>" token that can be used with other config file settings. The project folder
|
||||
* typically contains the tsconfig.json and package.json config files, but the path is user-defined.
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting.
|
||||
*
|
||||
* The default value for "projectFolder" is the token "<lookup>", which means the folder is determined by traversing
|
||||
* parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder
|
||||
* that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error
|
||||
* will be reported.
|
||||
*
|
||||
* SUPPORTED TOKENS: <lookup>
|
||||
* DEFAULT VALUE: "<lookup>"
|
||||
*/
|
||||
// "projectFolder": "..",
|
||||
|
||||
/**
|
||||
* (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor
|
||||
* analyzes the symbols exported by this module.
|
||||
*
|
||||
* The file extension must be ".d.ts" and not ".ts".
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
||||
* prepend a folder token such as "<projectFolder>".
|
||||
*
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
*/
|
||||
"mainEntryPointFilePath": "<projectFolder>/dist/thirdweb-dev-react-core.cjs.d.ts",
|
||||
|
||||
/**
|
||||
* A list of NPM package names whose exports should be treated as part of this package.
|
||||
*
|
||||
* For example, suppose that Webpack is used to generate a distributed bundle for the project "library1",
|
||||
* and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part
|
||||
* of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly
|
||||
* imports library2. To avoid this, we can specify:
|
||||
*
|
||||
* "bundledPackages": [ "library2" ],
|
||||
*
|
||||
* This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been
|
||||
* local files for library1.
|
||||
*/
|
||||
"bundledPackages": [],
|
||||
|
||||
/**
|
||||
* Determines how the TypeScript compiler engine will be invoked by API Extractor.
|
||||
*/
|
||||
"compiler": {
|
||||
/**
|
||||
* Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project.
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
||||
* prepend a folder token such as "<projectFolder>".
|
||||
*
|
||||
* Note: This setting will be ignored if "overrideTsconfig" is used.
|
||||
*
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
* DEFAULT VALUE: "<projectFolder>/tsconfig.json"
|
||||
*/
|
||||
// "tsconfigFilePath": "<projectFolder>/tsconfig.json",
|
||||
/**
|
||||
* Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk.
|
||||
* The object must conform to the TypeScript tsconfig schema:
|
||||
*
|
||||
* http://json.schemastore.org/tsconfig
|
||||
*
|
||||
* If omitted, then the tsconfig.json file will be read from the "projectFolder".
|
||||
*
|
||||
* DEFAULT VALUE: no overrideTsconfig section
|
||||
*/
|
||||
"overrideTsconfig": {
|
||||
"skipLibCheck": true
|
||||
}
|
||||
/**
|
||||
* This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended
|
||||
* and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when
|
||||
* dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses
|
||||
* for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck.
|
||||
*
|
||||
* DEFAULT VALUE: false
|
||||
*/
|
||||
// "skipLibCheck": true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Configures how the API report file (*.api.md) will be generated.
|
||||
*/
|
||||
"apiReport": {
|
||||
/**
|
||||
* (REQUIRED) Whether to generate an API report.
|
||||
*/
|
||||
"enabled": true
|
||||
|
||||
/**
|
||||
* The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce
|
||||
* a full file path.
|
||||
*
|
||||
* The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/".
|
||||
*
|
||||
* SUPPORTED TOKENS: <packageName>, <unscopedPackageName>
|
||||
* DEFAULT VALUE: "<unscopedPackageName>.api.md"
|
||||
*/
|
||||
// "reportFileName": "<unscopedPackageName>.api.md",
|
||||
|
||||
/**
|
||||
* Specifies the folder where the API report file is written. The file name portion is determined by
|
||||
* the "reportFileName" setting.
|
||||
*
|
||||
* The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy,
|
||||
* e.g. for an API review.
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
||||
* prepend a folder token such as "<projectFolder>".
|
||||
*
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
* DEFAULT VALUE: "<projectFolder>/etc/"
|
||||
*/
|
||||
// "reportFolder": "<projectFolder>/etc/",
|
||||
|
||||
/**
|
||||
* Specifies the folder where the temporary report file is written. The file name portion is determined by
|
||||
* the "reportFileName" setting.
|
||||
*
|
||||
* After the temporary file is written to disk, it is compared with the file in the "reportFolder".
|
||||
* If they are different, a production build will fail.
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
||||
* prepend a folder token such as "<projectFolder>".
|
||||
*
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
* DEFAULT VALUE: "<projectFolder>/temp/"
|
||||
*/
|
||||
// "reportTempFolder": "<projectFolder>/temp/"
|
||||
},
|
||||
|
||||
/**
|
||||
* Configures how the doc model file (*.api.json) will be generated.
|
||||
*/
|
||||
"docModel": {
|
||||
/**
|
||||
* (REQUIRED) Whether to generate a doc model file.
|
||||
*/
|
||||
"enabled": true
|
||||
|
||||
/**
|
||||
* The output path for the doc model file. The file extension should be ".api.json".
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
||||
* prepend a folder token such as "<projectFolder>".
|
||||
*
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
* DEFAULT VALUE: "<projectFolder>/temp/<unscopedPackageName>.api.json"
|
||||
*/
|
||||
// "apiJsonFilePath": "<projectFolder>/temp/<unscopedPackageName>.api.json"
|
||||
},
|
||||
|
||||
/**
|
||||
* Configures how the .d.ts rollup file will be generated.
|
||||
*/
|
||||
"dtsRollup": {
|
||||
/**
|
||||
* (REQUIRED) Whether to generate the .d.ts rollup file.
|
||||
*/
|
||||
"enabled": false,
|
||||
|
||||
/**
|
||||
* Specifies the output path for a .d.ts rollup file to be generated without any trimming.
|
||||
* This file will include all declarations that are exported by the main entry point.
|
||||
*
|
||||
* If the path is an empty string, then this file will not be written.
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
||||
* prepend a folder token such as "<projectFolder>".
|
||||
*
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
* DEFAULT VALUE: "<projectFolder>/dist/<unscopedPackageName>.d.ts"
|
||||
*/
|
||||
"untrimmedFilePath": "<projectFolder>/lib/<unscopedPackageName>.d.ts"
|
||||
|
||||
/**
|
||||
* Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release.
|
||||
* This file will include only declarations that are marked as "@public" or "@beta".
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
||||
* prepend a folder token such as "<projectFolder>".
|
||||
*
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
* DEFAULT VALUE: ""
|
||||
*/
|
||||
// "betaTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-beta.d.ts",
|
||||
|
||||
/**
|
||||
* Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release.
|
||||
* This file will include only declarations that are marked as "@public".
|
||||
*
|
||||
* If the path is an empty string, then this file will not be written.
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
||||
* prepend a folder token such as "<projectFolder>".
|
||||
*
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
* DEFAULT VALUE: ""
|
||||
*/
|
||||
// "publicTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-public.d.ts",
|
||||
|
||||
/**
|
||||
* When a declaration is trimmed, by default it will be replaced by a code comment such as
|
||||
* "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the
|
||||
* declaration completely.
|
||||
*
|
||||
* DEFAULT VALUE: false
|
||||
*/
|
||||
// "omitTrimmingComments": true
|
||||
},
|
||||
|
||||
/**
|
||||
* Configures how the tsdoc-metadata.json file will be generated.
|
||||
*/
|
||||
"tsdocMetadata": {
|
||||
/**
|
||||
* Whether to generate the tsdoc-metadata.json file.
|
||||
*
|
||||
* DEFAULT VALUE: true
|
||||
*/
|
||||
// "enabled": true,
|
||||
/**
|
||||
* Specifies where the TSDoc metadata file should be written.
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
||||
* prepend a folder token such as "<projectFolder>".
|
||||
*
|
||||
* The default value is "<lookup>", which causes the path to be automatically inferred from the "tsdocMetadata",
|
||||
* "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup
|
||||
* falls back to "tsdoc-metadata.json" in the package folder.
|
||||
*
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
* DEFAULT VALUE: "<lookup>"
|
||||
*/
|
||||
// "tsdocMetadataFilePath": "<projectFolder>/dist/tsdoc-metadata.json"
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies what type of newlines API Extractor should use when writing output files. By default, the output files
|
||||
* will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead.
|
||||
* To use the OS's default newline kind, specify "os".
|
||||
*
|
||||
* DEFAULT VALUE: "crlf"
|
||||
*/
|
||||
// "newlineKind": "crlf",
|
||||
|
||||
/**
|
||||
* Configures how API Extractor reports error and warning messages produced during analysis.
|
||||
*
|
||||
* There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages.
|
||||
*/
|
||||
"messages": {
|
||||
/**
|
||||
* Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing
|
||||
* the input .d.ts files.
|
||||
*
|
||||
* TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551"
|
||||
*
|
||||
* DEFAULT VALUE: A single "default" entry with logLevel=warning.
|
||||
*/
|
||||
"compilerMessageReporting": {
|
||||
/**
|
||||
* Configures the default routing for messages that don't match an explicit rule in this table.
|
||||
*/
|
||||
"default": {
|
||||
/**
|
||||
* Specifies whether the message should be written to the the tool's output log. Note that
|
||||
* the "addToApiReportFile" property may supersede this option.
|
||||
*
|
||||
* Possible values: "error", "warning", "none"
|
||||
*
|
||||
* Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail
|
||||
* and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes
|
||||
* the "--local" option), the warning is displayed but the build will not fail.
|
||||
*
|
||||
* DEFAULT VALUE: "warning"
|
||||
*/
|
||||
"logLevel": "warning"
|
||||
|
||||
/**
|
||||
* When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md),
|
||||
* then the message will be written inside that file; otherwise, the message is instead logged according to
|
||||
* the "logLevel" option.
|
||||
*
|
||||
* DEFAULT VALUE: false
|
||||
*/
|
||||
// "addToApiReportFile": false
|
||||
}
|
||||
|
||||
// "TS2551": {
|
||||
// "logLevel": "warning",
|
||||
// "addToApiReportFile": true
|
||||
// },
|
||||
//
|
||||
// . . .
|
||||
},
|
||||
|
||||
/**
|
||||
* Configures handling of messages reported by API Extractor during its analysis.
|
||||
*
|
||||
* API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag"
|
||||
*
|
||||
* DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings
|
||||
*/
|
||||
"extractorMessageReporting": {
|
||||
"default": {
|
||||
"logLevel": "warning"
|
||||
// "addToApiReportFile": false
|
||||
}
|
||||
|
||||
// "ae-extra-release-tag": {
|
||||
// "logLevel": "warning",
|
||||
// "addToApiReportFile": true
|
||||
// },
|
||||
//
|
||||
// . . .
|
||||
},
|
||||
|
||||
/**
|
||||
* Configures handling of messages reported by the TSDoc parser when analyzing code comments.
|
||||
*
|
||||
* TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text"
|
||||
*
|
||||
* DEFAULT VALUE: A single "default" entry with logLevel=warning.
|
||||
*/
|
||||
"tsdocMessageReporting": {
|
||||
"default": {
|
||||
"logLevel": "warning"
|
||||
// "addToApiReportFile": false
|
||||
}
|
||||
|
||||
// "tsdoc-link-tag-unescaped-text": {
|
||||
// "logLevel": "warning",
|
||||
// "addToApiReportFile": true
|
||||
// },
|
||||
//
|
||||
// . . .
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* Config file for API Extractor. For more info, please visit: https://api-extractor.com
|
||||
*/
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
|
||||
/**
|
||||
* Optionally specifies another JSON config file that this file extends from. This provides a way for
|
||||
* standard settings to be shared across multiple projects.
|
||||
*
|
||||
* If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains
|
||||
* the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be
|
||||
* resolved using NodeJS require().
|
||||
*
|
||||
* SUPPORTED TOKENS: none
|
||||
* DEFAULT VALUE: ""
|
||||
*/
|
||||
// "extends": "./shared/api-extractor-base.json"
|
||||
// "extends": "my-package/include/api-extractor-base.json"
|
||||
/**
|
||||
* Determines the "<projectFolder>" token that can be used with other config file settings. The project folder
|
||||
* typically contains the tsconfig.json and package.json config files, but the path is user-defined.
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting.
|
||||
*
|
||||
* The default value for "projectFolder" is the token "<lookup>", which means the folder is determined by traversing
|
||||
* parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder
|
||||
* that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error
|
||||
* will be reported.
|
||||
*
|
||||
* SUPPORTED TOKENS: <lookup>
|
||||
* DEFAULT VALUE: "<lookup>"
|
||||
*/
|
||||
// "projectFolder": "..",
|
||||
/**
|
||||
* (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor
|
||||
* analyzes the symbols exported by this module.
|
||||
*
|
||||
* The file extension must be ".d.ts" and not ".ts".
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
||||
* prepend a folder token such as "<projectFolder>".
|
||||
*
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
*/
|
||||
"mainEntryPointFilePath": "<projectFolder>/solana/dist/thirdweb-dev-react-core-solana.cjs.d.ts",
|
||||
/**
|
||||
* A list of NPM package names whose exports should be treated as part of this package.
|
||||
*
|
||||
* For example, suppose that Webpack is used to generate a distributed bundle for the project "library1",
|
||||
* and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part
|
||||
* of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly
|
||||
* imports library2. To avoid this, we can specify:
|
||||
*
|
||||
* "bundledPackages": [ "library2" ],
|
||||
*
|
||||
* This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been
|
||||
* local files for library1.
|
||||
*/
|
||||
"bundledPackages": [],
|
||||
/**
|
||||
* Determines how the TypeScript compiler engine will be invoked by API Extractor.
|
||||
*/
|
||||
"compiler": {
|
||||
/**
|
||||
* Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project.
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
||||
* prepend a folder token such as "<projectFolder>".
|
||||
*
|
||||
* Note: This setting will be ignored if "overrideTsconfig" is used.
|
||||
*
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
* DEFAULT VALUE: "<projectFolder>/tsconfig.json"
|
||||
*/
|
||||
// "tsconfigFilePath": "<projectFolder>/tsconfig.json",
|
||||
/**
|
||||
* Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk.
|
||||
* The object must conform to the TypeScript tsconfig schema:
|
||||
*
|
||||
* http://json.schemastore.org/tsconfig
|
||||
*
|
||||
* If omitted, then the tsconfig.json file will be read from the "projectFolder".
|
||||
*
|
||||
* DEFAULT VALUE: no overrideTsconfig section
|
||||
*/
|
||||
"overrideTsconfig": {
|
||||
"skipLibCheck": true
|
||||
}
|
||||
/**
|
||||
* This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended
|
||||
* and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when
|
||||
* dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses
|
||||
* for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck.
|
||||
*
|
||||
* DEFAULT VALUE: false
|
||||
*/
|
||||
// "skipLibCheck": true,
|
||||
},
|
||||
/**
|
||||
* Configures how the API report file (*.api.md) will be generated.
|
||||
*/
|
||||
"apiReport": {
|
||||
/**
|
||||
* (REQUIRED) Whether to generate an API report.
|
||||
*/
|
||||
"enabled": true
|
||||
/**
|
||||
* The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce
|
||||
* a full file path.
|
||||
*
|
||||
* The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/".
|
||||
*
|
||||
* SUPPORTED TOKENS: <packageName>, <unscopedPackageName>
|
||||
* DEFAULT VALUE: "<unscopedPackageName>.api.md"
|
||||
*/
|
||||
// "reportFileName": "<unscopedPackageName>.api.md",
|
||||
/**
|
||||
* Specifies the folder where the API report file is written. The file name portion is determined by
|
||||
* the "reportFileName" setting.
|
||||
*
|
||||
* The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy,
|
||||
* e.g. for an API review.
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
||||
* prepend a folder token such as "<projectFolder>".
|
||||
*
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
* DEFAULT VALUE: "<projectFolder>/etc/"
|
||||
*/
|
||||
// "reportFolder": "<projectFolder>/etc/",
|
||||
/**
|
||||
* Specifies the folder where the temporary report file is written. The file name portion is determined by
|
||||
* the "reportFileName" setting.
|
||||
*
|
||||
* After the temporary file is written to disk, it is compared with the file in the "reportFolder".
|
||||
* If they are different, a production build will fail.
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
||||
* prepend a folder token such as "<projectFolder>".
|
||||
*
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
* DEFAULT VALUE: "<projectFolder>/temp/"
|
||||
*/
|
||||
// "reportTempFolder": "<projectFolder>/temp/"
|
||||
},
|
||||
/**
|
||||
* Configures how the doc model file (*.api.json) will be generated.
|
||||
*/
|
||||
"docModel": {
|
||||
/**
|
||||
* (REQUIRED) Whether to generate a doc model file.
|
||||
*/
|
||||
"enabled": true
|
||||
/**
|
||||
* The output path for the doc model file. The file extension should be ".api.json".
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
||||
* prepend a folder token such as "<projectFolder>".
|
||||
*
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
* DEFAULT VALUE: "<projectFolder>/temp/<unscopedPackageName>.api.json"
|
||||
*/
|
||||
// "apiJsonFilePath": "<projectFolder>/temp/<unscopedPackageName>.api.json"
|
||||
},
|
||||
/**
|
||||
* Configures how the .d.ts rollup file will be generated.
|
||||
*/
|
||||
"dtsRollup": {
|
||||
/**
|
||||
* (REQUIRED) Whether to generate the .d.ts rollup file.
|
||||
*/
|
||||
"enabled": false,
|
||||
/**
|
||||
* Specifies the output path for a .d.ts rollup file to be generated without any trimming.
|
||||
* This file will include all declarations that are exported by the main entry point.
|
||||
*
|
||||
* If the path is an empty string, then this file will not be written.
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
||||
* prepend a folder token such as "<projectFolder>".
|
||||
*
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
* DEFAULT VALUE: "<projectFolder>/dist/<unscopedPackageName>.d.ts"
|
||||
*/
|
||||
"untrimmedFilePath": "<projectFolder>/lib/<unscopedPackageName>.d.ts"
|
||||
/**
|
||||
* Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release.
|
||||
* This file will include only declarations that are marked as "@public" or "@beta".
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
||||
* prepend a folder token such as "<projectFolder>".
|
||||
*
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
* DEFAULT VALUE: ""
|
||||
*/
|
||||
// "betaTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-beta.d.ts",
|
||||
/**
|
||||
* Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release.
|
||||
* This file will include only declarations that are marked as "@public".
|
||||
*
|
||||
* If the path is an empty string, then this file will not be written.
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
||||
* prepend a folder token such as "<projectFolder>".
|
||||
*
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
* DEFAULT VALUE: ""
|
||||
*/
|
||||
// "publicTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-public.d.ts",
|
||||
/**
|
||||
* When a declaration is trimmed, by default it will be replaced by a code comment such as
|
||||
* "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the
|
||||
* declaration completely.
|
||||
*
|
||||
* DEFAULT VALUE: false
|
||||
*/
|
||||
// "omitTrimmingComments": true
|
||||
},
|
||||
/**
|
||||
* Configures how the tsdoc-metadata.json file will be generated.
|
||||
*/
|
||||
"tsdocMetadata": {
|
||||
/**
|
||||
* Whether to generate the tsdoc-metadata.json file.
|
||||
*
|
||||
* DEFAULT VALUE: true
|
||||
*/
|
||||
// "enabled": true,
|
||||
/**
|
||||
* Specifies where the TSDoc metadata file should be written.
|
||||
*
|
||||
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
|
||||
* prepend a folder token such as "<projectFolder>".
|
||||
*
|
||||
* The default value is "<lookup>", which causes the path to be automatically inferred from the "tsdocMetadata",
|
||||
* "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup
|
||||
* falls back to "tsdoc-metadata.json" in the package folder.
|
||||
*
|
||||
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
|
||||
* DEFAULT VALUE: "<lookup>"
|
||||
*/
|
||||
// "tsdocMetadataFilePath": "<projectFolder>/dist/tsdoc-metadata.json"
|
||||
},
|
||||
/**
|
||||
* Specifies what type of newlines API Extractor should use when writing output files. By default, the output files
|
||||
* will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead.
|
||||
* To use the OS's default newline kind, specify "os".
|
||||
*
|
||||
* DEFAULT VALUE: "crlf"
|
||||
*/
|
||||
// "newlineKind": "crlf",
|
||||
/**
|
||||
* Configures how API Extractor reports error and warning messages produced during analysis.
|
||||
*
|
||||
* There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages.
|
||||
*/
|
||||
"messages": {
|
||||
/**
|
||||
* Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing
|
||||
* the input .d.ts files.
|
||||
*
|
||||
* TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551"
|
||||
*
|
||||
* DEFAULT VALUE: A single "default" entry with logLevel=warning.
|
||||
*/
|
||||
"compilerMessageReporting": {
|
||||
/**
|
||||
* Configures the default routing for messages that don't match an explicit rule in this table.
|
||||
*/
|
||||
"default": {
|
||||
/**
|
||||
* Specifies whether the message should be written to the the tool's output log. Note that
|
||||
* the "addToApiReportFile" property may supersede this option.
|
||||
*
|
||||
* Possible values: "error", "warning", "none"
|
||||
*
|
||||
* Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail
|
||||
* and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes
|
||||
* the "--local" option), the warning is displayed but the build will not fail.
|
||||
*
|
||||
* DEFAULT VALUE: "warning"
|
||||
*/
|
||||
"logLevel": "warning"
|
||||
/**
|
||||
* When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md),
|
||||
* then the message will be written inside that file; otherwise, the message is instead logged according to
|
||||
* the "logLevel" option.
|
||||
*
|
||||
* DEFAULT VALUE: false
|
||||
*/
|
||||
// "addToApiReportFile": false
|
||||
}
|
||||
// "TS2551": {
|
||||
// "logLevel": "warning",
|
||||
// "addToApiReportFile": true
|
||||
// },
|
||||
//
|
||||
// . . .
|
||||
},
|
||||
/**
|
||||
* Configures handling of messages reported by API Extractor during its analysis.
|
||||
*
|
||||
* API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag"
|
||||
*
|
||||
* DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings
|
||||
*/
|
||||
"extractorMessageReporting": {
|
||||
"default": {
|
||||
"logLevel": "warning"
|
||||
// "addToApiReportFile": false
|
||||
}
|
||||
// "ae-extra-release-tag": {
|
||||
// "logLevel": "warning",
|
||||
// "addToApiReportFile": true
|
||||
// },
|
||||
//
|
||||
// . . .
|
||||
},
|
||||
/**
|
||||
* Configures handling of messages reported by the TSDoc parser when analyzing code comments.
|
||||
*
|
||||
* TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text"
|
||||
*
|
||||
* DEFAULT VALUE: A single "default" entry with logLevel=warning.
|
||||
*/
|
||||
"tsdocMessageReporting": {
|
||||
"default": {
|
||||
"logLevel": "warning"
|
||||
// "addToApiReportFile": false
|
||||
}
|
||||
// "tsdoc-link-tag-unescaped-text": {
|
||||
// "logLevel": "warning",
|
||||
// "addToApiReportFile": true
|
||||
// },
|
||||
//
|
||||
// . . .
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"main": "dist/thirdweb-dev-react-core-evm.cjs.js",
|
||||
"module": "dist/thirdweb-dev-react-core-evm.esm.js"
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/react-core",
|
||||
"version": "3.7.3",
|
||||
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react-core",
|
||||
"author": "thirdweb eng <[email protected]>",
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/thirdweb-dev-react-core.cjs.js",
|
||||
"types": "dist/thirdweb-dev-react-core.cjs.d.ts",
|
||||
"module": "dist/thirdweb-dev-react-core.esm.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"module": "./dist/thirdweb-dev-react-core.esm.js",
|
||||
"default": "./dist/thirdweb-dev-react-core.cjs.js"
|
||||
},
|
||||
"./evm": {
|
||||
"module": "./evm/dist/thirdweb-dev-react-core-evm.esm.js",
|
||||
"default": "./evm/dist/thirdweb-dev-react-core-evm.cjs.js"
|
||||
},
|
||||
"./solana": {
|
||||
"module": "./solana/dist/thirdweb-dev-react-core-solana.esm.js",
|
||||
"default": "./solana/dist/thirdweb-dev-react-core-solana.cjs.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
"evm/",
|
||||
"solana/"
|
||||
],
|
||||
"preconstruct": {
|
||||
"entrypoints": [
|
||||
"index.ts",
|
||||
"solana/index.ts",
|
||||
"evm/index.ts"
|
||||
],
|
||||
"___experimentalFlags_WILL_CHANGE_IN_PATCH": {
|
||||
"exports": true
|
||||
},
|
||||
"exports": true
|
||||
},
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"format": "prettier --write 'src/**/*'",
|
||||
"lint": "eslint src/",
|
||||
"fix": "eslint src/ --fix",
|
||||
"clean": "rm -rf dist/",
|
||||
"generate-docs": "yarn generate-docs:evm && yarn generate-docs:solana && yarn generate-snippets",
|
||||
"generate-docs:evm": "api-extractor run --local --config ./config/api-extractor-evm.json && api-documenter markdown -i ./temp -o ./docs/evm && rm -rf ./temp-evm && mv ./temp ./temp-evm",
|
||||
"generate-docs:solana": "api-extractor run --local --config ./config/api-extractor-solana.json && api-documenter markdown -i ./temp -o ./docs/solana && rm -rf ./temp-solana && mv ./temp ./temp-solana",
|
||||
"generate-snippets": "node ./scripts/generate-snippets-evm.mjs && node ./scripts/generate-snippets-solana.mjs && node ./scripts/generate-feature-snippets-evm.mjs",
|
||||
"build": "tsc && preconstruct build",
|
||||
"push": "yalc push"
|
||||
},
|
||||
"sideEffects:": false,
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.3",
|
||||
"@babel/preset-env": "^7.18.10",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@babel/runtime": "^7.19.0",
|
||||
"@microsoft/api-documenter": "^7.19.4",
|
||||
"@microsoft/api-extractor": "^7.29.2",
|
||||
"@microsoft/tsdoc": "^0.14.1",
|
||||
"@preconstruct/cli": "^2.2.1",
|
||||
"@solana/web3.js": "^1.62.0",
|
||||
"@thirdweb-dev/sdk": "*",
|
||||
"@types/react": "^18.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.33.0",
|
||||
"@typescript-eslint/parser": "^5.33.0",
|
||||
"eslint": "^8.21.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-inclusive-language": "^2.2.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-tsdoc": "^0.2.16",
|
||||
"ethers": "^5.7.2",
|
||||
"prettier": "^2.7.1",
|
||||
"react": "^18.2.0",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^4.0.10",
|
||||
"tiny-invariant": "^1.2.0",
|
||||
"@thirdweb-dev/auth": "*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@solana/wallet-adapter-react": "^0.15.19",
|
||||
"@solana/web3.js": "^1.62.0",
|
||||
"@thirdweb-dev/sdk": "*",
|
||||
"ethers": ">=5.5.1",
|
||||
"react": ">=18.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"ansi-regex": "^6.0.1",
|
||||
"typescript": "^4.6.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@solana/web3.js": {
|
||||
"optional": true
|
||||
},
|
||||
"@solana/wallet-adapter-react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import {
|
||||
TSDocParser,
|
||||
DocExcerpt,
|
||||
TSDocConfiguration,
|
||||
TSDocTagSyntaxKind,
|
||||
TSDocTagDefinition,
|
||||
} from "@microsoft/tsdoc";
|
||||
import fs from "fs";
|
||||
|
||||
/**
|
||||
* This is a simplistic solution until we implement proper DocNode rendering APIs.
|
||||
*/
|
||||
export class Formatter {
|
||||
static renderDocNode(docNode) {
|
||||
let result = "";
|
||||
if (docNode) {
|
||||
if (docNode instanceof DocExcerpt) {
|
||||
result += docNode.content.toString();
|
||||
}
|
||||
for (const childNode of docNode.getChildNodes()) {
|
||||
result += Formatter.renderDocNode(childNode);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static renderDocNodes(docNodes) {
|
||||
let result = "";
|
||||
for (const docNode of docNodes) {
|
||||
result += Formatter.renderDocNode(docNode);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const config = new TSDocConfiguration();
|
||||
const tag = new TSDocTagDefinition({
|
||||
tagName: "@twfeature",
|
||||
allowMultiple: true,
|
||||
syntaxKind: TSDocTagSyntaxKind.BlockTag,
|
||||
});
|
||||
config.addTagDefinition(tag);
|
||||
config.setSupportForTag(tag, true);
|
||||
const tsdocParser = new TSDocParser(config);
|
||||
|
||||
const json = JSON.parse(
|
||||
fs.readFileSync(`${process.cwd()}/temp-evm/react-core.api.json`, "utf8"),
|
||||
);
|
||||
|
||||
function languageNameToKey(languageName) {
|
||||
switch (languageName) {
|
||||
case "js":
|
||||
case "jsx":
|
||||
return "javascript";
|
||||
case "ts":
|
||||
case "tsx":
|
||||
return "tyepscript";
|
||||
default:
|
||||
return languageName;
|
||||
}
|
||||
}
|
||||
|
||||
// Get all the DetectableFeature classes
|
||||
const classes = json.members[0].members;
|
||||
const features = {};
|
||||
|
||||
function parseExampleTag(docComment) {
|
||||
const exampleBlocks = docComment._customBlocks.filter(
|
||||
(b) => b._blockTag._tagName === "@example",
|
||||
);
|
||||
|
||||
const examplesString = Formatter.renderDocNodes(exampleBlocks);
|
||||
|
||||
const regex = /```([a-zA-Z]*)\n([\S\s]*?)\n```/g;
|
||||
|
||||
let matches;
|
||||
|
||||
const examples = {};
|
||||
|
||||
while ((matches = regex.exec(examplesString)) !== null) {
|
||||
// This is necessary to avoid infinite loops with zero-width matches
|
||||
if (matches.index === regex.lastIndex) {
|
||||
regex.lastIndex++;
|
||||
}
|
||||
examples[languageNameToKey(matches[1])] = matches[2];
|
||||
}
|
||||
return examples;
|
||||
}
|
||||
|
||||
function parseTWFeatureTag(docComment) {
|
||||
const featureBlocks = docComment._customBlocks.filter(
|
||||
(b) => b._blockTag._tagName === "@twfeature",
|
||||
);
|
||||
|
||||
const featuresString = Formatter.renderDocNodes(featureBlocks);
|
||||
return featuresString.split(" | ").map((f) =>
|
||||
f
|
||||
.trim()
|
||||
.replace(/\\n/g, "")
|
||||
.replace("\n\n", "")
|
||||
.replace(/`/g, "")
|
||||
.replace(/@twfeature/g, ""),
|
||||
);
|
||||
}
|
||||
|
||||
const baseDocUrl = "https://docs.thirdweb.com/react/react.";
|
||||
|
||||
const extractReferenceLink = (m, kind, contractName) => {
|
||||
if (kind === "Property") {
|
||||
return m.excerptTokens
|
||||
.filter((e) => e.kind === "Reference")
|
||||
.map((e) => `${baseDocUrl}${e.text.toLowerCase()}`)[0];
|
||||
}
|
||||
if (kind === "Method") {
|
||||
return `${baseDocUrl}${contractName}.${m.name}`.toLowerCase();
|
||||
}
|
||||
return `${baseDocUrl}${m.name}`.toLowerCase();
|
||||
};
|
||||
|
||||
const parseMembers = (members, kind, contractName) => {
|
||||
if (!members) {
|
||||
return [];
|
||||
}
|
||||
|
||||
members = [members];
|
||||
|
||||
const validMembers = members.filter((m) => m.kind === kind);
|
||||
return validMembers
|
||||
.map((m) => {
|
||||
const parserContext = tsdocParser.parseString(m.docComment);
|
||||
const docComment = parserContext.docComment;
|
||||
const featureNames = parseTWFeatureTag(docComment);
|
||||
if (featureNames.length > 0) {
|
||||
for (const feature of featureNames) {
|
||||
const examples = parseExampleTag(docComment);
|
||||
if (Object.keys(examples).length > 0) {
|
||||
const example = {
|
||||
name: m.name,
|
||||
summary: Formatter.renderDocNode(docComment.summarySection),
|
||||
remarks: docComment.remarksBlock
|
||||
? Formatter.renderDocNode(docComment.remarksBlock.content)
|
||||
: null,
|
||||
examples,
|
||||
reference: {
|
||||
javascript: extractReferenceLink(m, kind, contractName),
|
||||
},
|
||||
};
|
||||
features[feature] = features[feature]
|
||||
? features[feature].concat(example)
|
||||
: [example];
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter((m) => !!m);
|
||||
};
|
||||
|
||||
classes.forEach((m) => {
|
||||
parseMembers(m, "Function", m.name);
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
`${process.cwd()}/docs/evm/feature_snippets.json`,
|
||||
JSON.stringify(features, null, 2),
|
||||
);
|
||||
@@ -0,0 +1,230 @@
|
||||
import { DocExcerpt, TSDocParser } from "@microsoft/tsdoc";
|
||||
import fs from "fs";
|
||||
|
||||
/**
|
||||
* This is a simplistic solution until we implement proper DocNode rendering APIs.
|
||||
*/
|
||||
export class Formatter {
|
||||
static renderDocNode(docNode) {
|
||||
let result = "";
|
||||
if (docNode) {
|
||||
if (docNode instanceof DocExcerpt) {
|
||||
result += docNode.content.toString();
|
||||
}
|
||||
for (const childNode of docNode.getChildNodes()) {
|
||||
result += Formatter.renderDocNode(childNode);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static renderDocNodes(docNodes) {
|
||||
let result = "";
|
||||
for (const docNode of docNodes) {
|
||||
result += Formatter.renderDocNode(docNode);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const NFT_HOOKS = [
|
||||
"useNFT",
|
||||
"useNFTs",
|
||||
"useTotalCirculatingSupply",
|
||||
"useOwnedNFTs",
|
||||
"useNFTBalance",
|
||||
"useMintNFT",
|
||||
];
|
||||
|
||||
const TOKEN_HOOKS = ["useTokenSupply", "useTokenBalance", "useMintToken"];
|
||||
|
||||
const MARKETPLACE_HOOKS = [
|
||||
"useListing",
|
||||
"useListings",
|
||||
"useActiveListings",
|
||||
"useWinningBid",
|
||||
"useAuctionWinner",
|
||||
"useBidBuffer",
|
||||
"useCreateDirectListing",
|
||||
"useCreateAuctionListing",
|
||||
"useMakeBid",
|
||||
"useBuyNow",
|
||||
];
|
||||
|
||||
const DROP_HOOKS = [
|
||||
"useUnclaimedNFTs",
|
||||
"useClaimedNFTs",
|
||||
"useUnclaimedNFTSupply",
|
||||
"useClaimedNFTSupply",
|
||||
"useClaimNFT",
|
||||
"useClaimToken",
|
||||
];
|
||||
|
||||
const CLAIM_CONDITIONS_HOOKS = [
|
||||
"useActiveClaimCondition",
|
||||
"useClaimConditions",
|
||||
"useClaimIneligibilityReasons",
|
||||
];
|
||||
|
||||
const WALLET_CONNECTION_HOOKS = [
|
||||
"useAddress",
|
||||
"useMetamask",
|
||||
"useWalletConnect",
|
||||
"useCoinbaseWallet",
|
||||
"useMagic",
|
||||
"useGnosis",
|
||||
"useDisconnect",
|
||||
];
|
||||
|
||||
const NETWORK_INFO_HOOKS = ["useChainId", "useNetwork", "useNetworkMismatch"];
|
||||
|
||||
const CUSTOM_CONTRACT_HOOKS = [
|
||||
"useContract",
|
||||
"useContractRead",
|
||||
"useContractWrite",
|
||||
"useAllContractEvents",
|
||||
"useContractEvents",
|
||||
"useContractType",
|
||||
"useContractPublishMetadata",
|
||||
"useContractMetadata",
|
||||
];
|
||||
|
||||
const CONTRACT_SETTINGS_HOOKS = [
|
||||
"useMetadata",
|
||||
"useUpdateMetadata",
|
||||
"usePrimarySalesRecipient",
|
||||
"useUpdatePrimarySaleRecipient",
|
||||
"useRoyaltySettings",
|
||||
"useUpdateRoyaltySettings",
|
||||
"usePlatformFees",
|
||||
"useUpdatePlatformFees",
|
||||
"useAllRoleMembers",
|
||||
"useRoleMembers",
|
||||
"useIsAddressRole",
|
||||
"useSetAllRoleMembers",
|
||||
"useGrantRole",
|
||||
"useRevokeRole",
|
||||
];
|
||||
|
||||
const HOOKS_CONFIG = {
|
||||
useSignatureDrop: [...NFT_HOOKS, ...DROP_HOOKS, ...CLAIM_CONDITIONS_HOOKS],
|
||||
useNFTDrop: [...NFT_HOOKS, ...DROP_HOOKS, ...CLAIM_CONDITIONS_HOOKS],
|
||||
useEditionDrop: [...NFT_HOOKS, ...DROP_HOOKS, ...CLAIM_CONDITIONS_HOOKS],
|
||||
useNFTCollection: [...NFT_HOOKS],
|
||||
useEdition: [...NFT_HOOKS],
|
||||
useTokenDrop: [...TOKEN_HOOKS, ...DROP_HOOKS, ...CLAIM_CONDITIONS_HOOKS],
|
||||
useToken: [...TOKEN_HOOKS],
|
||||
useMarketplace: [...MARKETPLACE_HOOKS],
|
||||
useSplit: [],
|
||||
useVote: [],
|
||||
usePack: [...NFT_HOOKS],
|
||||
useMultiwrap: [...NFT_HOOKS],
|
||||
useContract: [
|
||||
...WALLET_CONNECTION_HOOKS,
|
||||
...NETWORK_INFO_HOOKS,
|
||||
...CUSTOM_CONTRACT_HOOKS,
|
||||
...CONTRACT_SETTINGS_HOOKS,
|
||||
],
|
||||
};
|
||||
|
||||
const tsdocParser = new TSDocParser();
|
||||
|
||||
const json = JSON.parse(
|
||||
fs.readFileSync(`${process.cwd()}/temp-evm/react-core.api.json`, "utf8"),
|
||||
);
|
||||
|
||||
function languageNameToKey(languageName) {
|
||||
switch (languageName) {
|
||||
case "js":
|
||||
case "jsx":
|
||||
return "javascript";
|
||||
case "ts":
|
||||
case "tsx":
|
||||
return "typescript";
|
||||
default:
|
||||
return languageName;
|
||||
}
|
||||
}
|
||||
|
||||
const hooks = json.members[0].members.filter(
|
||||
(m) => m.kind === "Function" && Object.keys(HOOKS_CONFIG).includes(m.name),
|
||||
);
|
||||
|
||||
function parseExampleTag(docComment) {
|
||||
const exampleBlocks = docComment._customBlocks.filter(
|
||||
(b) => b._blockTag._tagName === "@example",
|
||||
);
|
||||
|
||||
const examplesString = Formatter.renderDocNodes(exampleBlocks);
|
||||
|
||||
const regex = /```([a-zA-Z]*)\n([\S\s]*?)\n```/g;
|
||||
|
||||
let matches;
|
||||
|
||||
const examples = {};
|
||||
|
||||
while ((matches = regex.exec(examplesString)) !== null) {
|
||||
// This is necessary to avoid infinite loops with zero-width matches
|
||||
if (matches.index === regex.lastIndex) {
|
||||
regex.lastIndex++;
|
||||
}
|
||||
|
||||
const re = /\n( *)\/\/ For example((.|\n)*)\.\.\.\n/g;
|
||||
const example = matches[2].replace(re, "");
|
||||
examples[languageNameToKey(matches[1])] = example;
|
||||
}
|
||||
return examples;
|
||||
}
|
||||
|
||||
const baseDocUrl = "https://portal.thirdweb.com/react/react.";
|
||||
|
||||
const extractReferenceLink = (m, kind, contractName) => {
|
||||
if (kind === "Property") {
|
||||
return m.excerptTokens
|
||||
.filter((e) => e.kind === "Reference")
|
||||
.map((e) => `${baseDocUrl}${e.text.toLowerCase()}`)[0];
|
||||
}
|
||||
if (kind === "Method") {
|
||||
return `${baseDocUrl}${contractName}.${m.name}`;
|
||||
}
|
||||
return `${baseDocUrl}${m.name}`;
|
||||
};
|
||||
|
||||
const moduleMap = hooks.reduce((acc, m) => {
|
||||
const parserContext = tsdocParser.parseString(m.docComment);
|
||||
const docComment = parserContext.docComment;
|
||||
const examples = parseExampleTag(docComment);
|
||||
|
||||
const contractSubhooks = json.members[0].members.filter(
|
||||
(f) => f.kind === "Function" && HOOKS_CONFIG[m.name].includes(f.name),
|
||||
);
|
||||
|
||||
const contractHooks = contractSubhooks.map((subhookContent) => {
|
||||
const subhookContext = tsdocParser.parseString(subhookContent.docComment);
|
||||
const subhookComment = subhookContext.docComment;
|
||||
const subhookExamples = parseExampleTag(subhookComment);
|
||||
|
||||
return {
|
||||
name: subhookContent.name,
|
||||
example: subhookExamples?.javascript || "",
|
||||
reference: `${baseDocUrl}${subhookContent.name.toLowerCase()}`,
|
||||
};
|
||||
});
|
||||
|
||||
if (Object.keys(examples).length > 0) {
|
||||
acc[m.name] = {
|
||||
name: m.name,
|
||||
summary: Formatter.renderDocNode(docComment.summarySection),
|
||||
examples,
|
||||
subhooks: contractHooks,
|
||||
reference: extractReferenceLink(m),
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
fs.writeFileSync(
|
||||
`${process.cwd()}/docs/evm/snippets.json`,
|
||||
JSON.stringify(moduleMap, null, 2),
|
||||
);
|
||||
@@ -0,0 +1,135 @@
|
||||
import { Formatter } from "./generate-snippets-evm.mjs";
|
||||
import { TSDocParser } from "@microsoft/tsdoc";
|
||||
import fs from "fs";
|
||||
|
||||
const NFT_HOOKS = ["useMintNFT", "useNFT", "useNFTs", "useTransferNFT"];
|
||||
|
||||
const DROP_HOOKS = [
|
||||
"useClaimedSupply",
|
||||
"useClaimNFT",
|
||||
"useLazyMintNFT",
|
||||
"useUnclaimedSupply",
|
||||
];
|
||||
|
||||
const TOKEN_HOOKS = [
|
||||
"useMintToken",
|
||||
"useTokenBalance",
|
||||
"useTokenSupply",
|
||||
"useTransferToken",
|
||||
];
|
||||
|
||||
const HOOKS_CONFIG = {
|
||||
useNFTCollection: [...NFT_HOOKS],
|
||||
useNFTDrop: [...NFT_HOOKS, ...DROP_HOOKS],
|
||||
useToken: [...TOKEN_HOOKS],
|
||||
useProgram: [],
|
||||
};
|
||||
|
||||
const tsdocParser = new TSDocParser();
|
||||
|
||||
const json = JSON.parse(
|
||||
fs.readFileSync(`${process.cwd()}/temp-solana/react-core.api.json`, "utf8"),
|
||||
);
|
||||
|
||||
function languageNameToKey(languageName) {
|
||||
switch (languageName) {
|
||||
case "js":
|
||||
case "jsx":
|
||||
return "javascript";
|
||||
case "ts":
|
||||
case "tsx":
|
||||
return "typescript";
|
||||
default:
|
||||
return languageName;
|
||||
}
|
||||
}
|
||||
|
||||
function parseExampleTag(docComment) {
|
||||
const exampleBlocks = docComment._customBlocks.filter(
|
||||
(b) => b._blockTag._tagName === "@example",
|
||||
);
|
||||
|
||||
const examplesString = Formatter.renderDocNodes(exampleBlocks);
|
||||
|
||||
const regex = /```([a-zA-Z]*)\n([\S\s]*?)\n```/g;
|
||||
|
||||
let matches;
|
||||
|
||||
const examples = {};
|
||||
|
||||
while ((matches = regex.exec(examplesString)) !== null) {
|
||||
// This is necessary to avoid infinite loops with zero-width matches
|
||||
if (matches.index === regex.lastIndex) {
|
||||
regex.lastIndex++;
|
||||
}
|
||||
|
||||
const re = /\n( *)\/\/ For example((.|\n)*)\.\.\.\n/g;
|
||||
const example = matches[2].replace(re, "");
|
||||
examples[languageNameToKey(matches[1])] = example;
|
||||
}
|
||||
return examples;
|
||||
}
|
||||
|
||||
const baseDocUrl = "https://portal.thirdweb.com/react/solana/react.";
|
||||
|
||||
const extractReferenceLink = (m, kind, contractName) => {
|
||||
if (kind === "Property") {
|
||||
return m.excerptTokens
|
||||
.filter((e) => e.kind === "Reference")
|
||||
.map((e) => `${baseDocUrl}${e.text.toLowerCase()}`)[0];
|
||||
}
|
||||
if (kind === "Method") {
|
||||
return `${baseDocUrl}${contractName}.${m.name}`;
|
||||
}
|
||||
return `${baseDocUrl}${m.name}`;
|
||||
};
|
||||
|
||||
const moduleMap = Object.keys(HOOKS_CONFIG).reduce((acc, hookName) => {
|
||||
let summary = "";
|
||||
let examples = {};
|
||||
let reference = "";
|
||||
|
||||
const doc = json.members[0].members.find(
|
||||
(m) =>
|
||||
m.kind === "Function" && Object.keys(HOOKS_CONFIG).includes(hookName),
|
||||
);
|
||||
|
||||
if (doc) {
|
||||
const parserContext = tsdocParser.parseString(doc);
|
||||
const docComment = parserContext.docComment;
|
||||
summary = Formatter.renderDocNode(docComment.summarySection);
|
||||
examples = parseExampleTag(docComment);
|
||||
reference = extractReferenceLink(doc);
|
||||
}
|
||||
|
||||
const contractSubhooks = json.members[0].members.filter(
|
||||
(f) => f.kind === "Function" && HOOKS_CONFIG[hookName].includes(f.name),
|
||||
);
|
||||
|
||||
const subhooks = contractSubhooks.map((subhookContent) => {
|
||||
const subhookContext = tsdocParser.parseString(subhookContent.docComment);
|
||||
const subhookComment = subhookContext.docComment;
|
||||
const subhookExamples = parseExampleTag(subhookComment);
|
||||
|
||||
return {
|
||||
name: subhookContent.name,
|
||||
example: subhookExamples?.javascript || "",
|
||||
reference: `${baseDocUrl}${subhookContent.name.toLowerCase()}`,
|
||||
};
|
||||
});
|
||||
|
||||
acc[hookName] = {
|
||||
name: hookName,
|
||||
summary,
|
||||
examples,
|
||||
subhooks,
|
||||
reference,
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
fs.writeFileSync(
|
||||
`${process.cwd()}/docs/solana/snippets.json`,
|
||||
JSON.stringify(moduleMap, null, 2),
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"main": "dist/thirdweb-dev-react-core-solana.cjs.js",
|
||||
"module": "dist/thirdweb-dev-react-core-solana.esm.js"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ComponentWithChildren } from "../types/component";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export interface QueryClientProviderProps {
|
||||
queryClient?: QueryClient;
|
||||
}
|
||||
|
||||
export const QueryClientProviderWithDefault: ComponentWithChildren<
|
||||
QueryClientProviderProps
|
||||
> = ({ queryClient, children }) => {
|
||||
const queryClientWithDefault: QueryClient = useMemo(() => {
|
||||
return queryClient ? queryClient : new QueryClient();
|
||||
}, [queryClient]);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClientWithDefault}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { RequiredParam } from "./required-param";
|
||||
import type { QueryKey } from "@tanstack/react-query";
|
||||
import type { ValidContractInstance } from "@thirdweb-dev/sdk";
|
||||
import type {
|
||||
Network,
|
||||
NFTCollection,
|
||||
NFTDrop,
|
||||
Token,
|
||||
} from "@thirdweb-dev/sdk/solana";
|
||||
|
||||
// we prefix all our query keys with this to avoid possible collisions with user-defined queries that share the same query client
|
||||
const TW_QUERY_KEY_PREFIX = "__tw__";
|
||||
// marker to make sure the query will not get stored in local storage by a query persister
|
||||
const NEVER_PERSIST_QUERY_POSTFIX = { persist: false } as const;
|
||||
|
||||
export function ensureTWPrefix<TKey extends QueryKey>(key: TKey) {
|
||||
if (key[0] === TW_QUERY_KEY_PREFIX) {
|
||||
return key as unknown as Readonly<[typeof TW_QUERY_KEY_PREFIX, ...TKey]>;
|
||||
}
|
||||
return [TW_QUERY_KEY_PREFIX, ...key] as const;
|
||||
}
|
||||
|
||||
export function neverPersist<TKey extends QueryKey>(key: TKey) {
|
||||
return [...key, NEVER_PERSIST_QUERY_POSTFIX] as const;
|
||||
}
|
||||
|
||||
export function shouldNeverPersistQuery<TKey extends QueryKey>(
|
||||
key: TKey,
|
||||
): boolean {
|
||||
return key[key.length - 1] === NEVER_PERSIST_QUERY_POSTFIX;
|
||||
}
|
||||
|
||||
// EVM
|
||||
|
||||
export function createEVMQueryKey<TKey extends QueryKey>(key: TKey) {
|
||||
return ensureTWPrefix(["evm", ...key] as const);
|
||||
}
|
||||
export function createEVMQueryKeyWithChain<TKey extends QueryKey>(
|
||||
key: TKey,
|
||||
chainId: RequiredParam<number>,
|
||||
) {
|
||||
return ensureTWPrefix([chainId, ...key] as const);
|
||||
}
|
||||
|
||||
export function createEVMContractQueryKey<TKey extends QueryKey>(
|
||||
contract: RequiredParam<ValidContractInstance>,
|
||||
key: TKey = [] as unknown as TKey,
|
||||
) {
|
||||
const chainId = contract?.chainId;
|
||||
const address = contract?.getAddress();
|
||||
return createEVMQueryKeyWithChain(
|
||||
["contract", address, ...key] as const,
|
||||
chainId,
|
||||
);
|
||||
}
|
||||
|
||||
// SOL
|
||||
|
||||
export function createSOLQueryKey<TKey extends QueryKey>(key: TKey) {
|
||||
return ensureTWPrefix(["sol", ...key] as const);
|
||||
}
|
||||
|
||||
export function createSOLQueryKeyWithNetwork<TKey extends QueryKey>(
|
||||
key: TKey,
|
||||
network: RequiredParam<Network>,
|
||||
) {
|
||||
return createSOLQueryKey([network, ...key] as const);
|
||||
}
|
||||
|
||||
export function createSOLProgramQueryKey<TKey extends QueryKey>(
|
||||
program: RequiredParam<NFTCollection | NFTDrop | Token>,
|
||||
key: TKey = [] as unknown as TKey,
|
||||
) {
|
||||
const network = program?.network;
|
||||
const address = program?.publicKey.toBase58();
|
||||
return createSOLQueryKeyWithNetwork(
|
||||
["program", address, ...key] as const,
|
||||
network || null,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
/**
|
||||
* Makes a parameter required to be passed, but still allowes it to be null or undefined.
|
||||
*
|
||||
* @beta
|
||||
*/
|
||||
export type RequiredParam<T> = T | null | undefined;
|
||||
|
||||
export function requiredParamInvariant<TValue>(
|
||||
condition: RequiredParam<TValue>,
|
||||
message?: string,
|
||||
): asserts condition {
|
||||
invariant(condition !== null || condition !== undefined, message);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import React from "react";
|
||||
|
||||
export type ComponentWithChildren<P = unknown> = React.FC<
|
||||
React.PropsWithChildren<P>
|
||||
>;
|
||||
@@ -0,0 +1,213 @@
|
||||
import { NATIVE_TOKENS, SUPPORTED_CHAIN_ID } from "@thirdweb-dev/sdk";
|
||||
import { ChainId } from "@thirdweb-dev/sdk";
|
||||
import { Chain } from "../types";
|
||||
|
||||
const chain: Record<string, Chain> = {
|
||||
mainnet: {
|
||||
id: ChainId.Mainnet,
|
||||
name: "Ethereum Mainnet",
|
||||
nativeCurrency: NATIVE_TOKENS[ChainId.Mainnet],
|
||||
rpcUrls: ["https://mainnet.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161"],
|
||||
blockExplorers: [
|
||||
{
|
||||
name: "Etherscan",
|
||||
url: "https://etherscan.io",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
goerli: {
|
||||
id: ChainId.Goerli,
|
||||
name: "Goerli",
|
||||
nativeCurrency: NATIVE_TOKENS[ChainId.Goerli],
|
||||
rpcUrls: ["https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161"],
|
||||
blockExplorers: [
|
||||
{
|
||||
name: "Etherscan",
|
||||
url: "https://goerli.etherscan.io",
|
||||
},
|
||||
],
|
||||
testnet: true,
|
||||
},
|
||||
polygonMainnet: {
|
||||
id: ChainId.Polygon,
|
||||
name: "Polygon Mainnet",
|
||||
nativeCurrency: NATIVE_TOKENS[ChainId.Polygon],
|
||||
rpcUrls: [
|
||||
"https://polygon-rpc.com",
|
||||
"https://rpc-mainnet.matic.network",
|
||||
"https://matic-mainnet.chainstacklabs.com",
|
||||
"https://rpc-mainnet.maticvigil.com",
|
||||
"https://rpc-mainnet.matic.quiknode.pro",
|
||||
"https://matic-mainnet-full-rpc.bwarelabs.com",
|
||||
],
|
||||
blockExplorers: [
|
||||
{
|
||||
name: "Polygonscan",
|
||||
url: "https://polygonscan.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
polygonTestnetMumbai: {
|
||||
id: ChainId.Mumbai,
|
||||
name: "Mumbai",
|
||||
nativeCurrency: NATIVE_TOKENS[ChainId.Mumbai],
|
||||
rpcUrls: [
|
||||
"https://matic-mumbai.chainstacklabs.com",
|
||||
"https://rpc-mumbai.maticvigil.com",
|
||||
"https://matic-testnet-archive-rpc.bwarelabs.com",
|
||||
],
|
||||
blockExplorers: [
|
||||
{
|
||||
name: "PolygonScan",
|
||||
url: "https://mumbai.polygonscan.com",
|
||||
},
|
||||
],
|
||||
testnet: true,
|
||||
},
|
||||
avalanche: {
|
||||
id: ChainId.Avalanche,
|
||||
name: "Avalanche",
|
||||
nativeCurrency: NATIVE_TOKENS[ChainId.Avalanche],
|
||||
rpcUrls: [
|
||||
"https://api.avax.network/ext/bc/C/rpc",
|
||||
"https://rpc.ankr.com/avalanche",
|
||||
],
|
||||
blockExplorers: [
|
||||
{
|
||||
name: "SnowTrace",
|
||||
url: "https://snowtrace.io/",
|
||||
},
|
||||
],
|
||||
testnet: false,
|
||||
},
|
||||
avalancheFujiTestnet: {
|
||||
id: ChainId.AvalancheFujiTestnet,
|
||||
name: "Avalanche Fuji Testnet",
|
||||
nativeCurrency: NATIVE_TOKENS[ChainId.AvalancheFujiTestnet],
|
||||
rpcUrls: ["https://api.avax-test.network/ext/bc/C/rpc"],
|
||||
blockExplorers: [
|
||||
{
|
||||
name: "SnowTrace",
|
||||
url: "https://testnet.snowtrace.io/",
|
||||
},
|
||||
],
|
||||
testnet: true,
|
||||
},
|
||||
fantom: {
|
||||
id: ChainId.Fantom,
|
||||
name: "Fantom Opera",
|
||||
nativeCurrency: NATIVE_TOKENS[ChainId.Fantom],
|
||||
rpcUrls: ["https://rpc.ftm.tools"],
|
||||
blockExplorers: [
|
||||
{
|
||||
name: "FTMscan",
|
||||
url: "https://ftmscan.com/",
|
||||
},
|
||||
],
|
||||
testnet: false,
|
||||
},
|
||||
fantomTestnet: {
|
||||
id: ChainId.FantomTestnet,
|
||||
name: "Fantom Testnet",
|
||||
nativeCurrency: NATIVE_TOKENS[ChainId.FantomTestnet],
|
||||
rpcUrls: ["https://rpc.testnet.fantom.network"],
|
||||
blockExplorers: [
|
||||
{
|
||||
name: "FTMscan",
|
||||
url: "https://testnet.ftmscan.com/",
|
||||
},
|
||||
],
|
||||
testnet: true,
|
||||
},
|
||||
optimism: {
|
||||
id: ChainId.Optimism,
|
||||
name: "Optimism",
|
||||
nativeCurrency: NATIVE_TOKENS[ChainId.Optimism],
|
||||
rpcUrls: ["https://mainnet.optimism.io"],
|
||||
blockExplorers: [
|
||||
{
|
||||
name: "Etherscan",
|
||||
url: "https://optimistic.etherscan.io/",
|
||||
},
|
||||
],
|
||||
testnet: false,
|
||||
},
|
||||
|
||||
optimismGoerli: {
|
||||
id: ChainId.OptimismGoerli,
|
||||
name: "Optimism Goerli Testnet",
|
||||
nativeCurrency: NATIVE_TOKENS[ChainId.OptimismGoerli],
|
||||
rpcUrls: ["https://goerli.optimism.io/"],
|
||||
blockExplorers: [
|
||||
{
|
||||
name: "Etherscan",
|
||||
url: "https://goerli-optimism.etherscan.io/",
|
||||
},
|
||||
],
|
||||
testnet: true,
|
||||
},
|
||||
arbitrum: {
|
||||
id: ChainId.Arbitrum,
|
||||
name: "Arbitrum One",
|
||||
nativeCurrency: NATIVE_TOKENS[ChainId.Arbitrum],
|
||||
rpcUrls: ["https://arb1.arbitrum.io/rpc"],
|
||||
blockExplorers: [
|
||||
{
|
||||
name: "Arbiscan",
|
||||
url: "https://arbiscan.io/",
|
||||
},
|
||||
],
|
||||
testnet: false,
|
||||
},
|
||||
|
||||
arbitrumGoerli: {
|
||||
id: ChainId.ArbitrumGoerli,
|
||||
name: "Arbitrum Goerli",
|
||||
nativeCurrency: NATIVE_TOKENS[ChainId.ArbitrumGoerli],
|
||||
rpcUrls: ["https://goerli-rollup.arbitrum.io/rpc/"],
|
||||
blockExplorers: [
|
||||
{
|
||||
name: "Arbitrum Goerli Rollup Explorer",
|
||||
url: "https://goerli-rollup-explorer.arbitrum.io",
|
||||
},
|
||||
],
|
||||
testnet: true,
|
||||
},
|
||||
binanceSmartChainMainnet: {
|
||||
id: ChainId.BinanceSmartChainMainnet,
|
||||
name: "Binance Smart Chain Mainnet",
|
||||
nativeCurrency: NATIVE_TOKENS[ChainId.BinanceSmartChainMainnet],
|
||||
rpcUrls: ["https://bsc-dataseed1.binance.org"],
|
||||
blockExplorers: [
|
||||
{
|
||||
name: "BscScan",
|
||||
url: "https://bscscan.com/",
|
||||
},
|
||||
],
|
||||
testnet: false,
|
||||
},
|
||||
binanceSmartChainTestnet: {
|
||||
id: ChainId.BinanceSmartChainTestnet,
|
||||
name: "Binance Smart Chain Testnet",
|
||||
nativeCurrency: NATIVE_TOKENS[ChainId.BinanceSmartChainTestnet],
|
||||
rpcUrls: ["https://data-seed-prebsc-1-s1.binance.org:8545"],
|
||||
blockExplorers: [
|
||||
{
|
||||
name: "BscScan",
|
||||
url: "https://testnet.bscscan.com/",
|
||||
},
|
||||
],
|
||||
testnet: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultSupportedChains = Object.values(chain) as Chain[];
|
||||
|
||||
export type SupportedChainId = typeof defaultSupportedChains[number]["id"];
|
||||
|
||||
export type SupportedChain = SupportedChainId | Chain;
|
||||
|
||||
export function getChainFromChainId(chainId: SUPPORTED_CHAIN_ID) {
|
||||
return defaultSupportedChains.find((c) => c.id === chainId) as Chain;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
export const __DEV__ = process.env.NODE_ENV !== "production";
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useSigner } from "../hooks/useSigner";
|
||||
import { ThirdwebAuth } from "@thirdweb-dev/auth";
|
||||
import { SignerWallet } from "@thirdweb-dev/auth/evm";
|
||||
import React, {
|
||||
PropsWithChildren,
|
||||
createContext,
|
||||
useContext,
|
||||
useMemo,
|
||||
} from "react";
|
||||
|
||||
/**
|
||||
* The configuration to use the react SDK with an [auth](https://portal.thirdweb.com/auth) server.
|
||||
*
|
||||
* @beta
|
||||
*/
|
||||
export interface ThirdwebAuthConfig {
|
||||
/**
|
||||
* The backend URL of the authentication endoints. For example, if your endpoints are
|
||||
* at `/api/auth/login`, `/api/auth/logout`, etc. then this should be set to `/api/auth`.
|
||||
*/
|
||||
authUrl: string;
|
||||
|
||||
/**
|
||||
* The frontend domain used to generate the login payload.
|
||||
* This domain should match the domain used on your auth backend.
|
||||
*/
|
||||
domain: string;
|
||||
}
|
||||
|
||||
interface ThirdwebAuthContext extends ThirdwebAuthConfig {
|
||||
auth?: ThirdwebAuth;
|
||||
}
|
||||
|
||||
const ThirdwebAuthContext = createContext<ThirdwebAuthContext | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export const ThirdwebAuthProvider: React.FC<
|
||||
PropsWithChildren<{ value?: ThirdwebAuthConfig }>
|
||||
> = ({ value, children }) => {
|
||||
const signer = useSigner();
|
||||
|
||||
// Remove trailing slash from URL if present
|
||||
const authContext = useMemo(() => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const context: ThirdwebAuthContext = {
|
||||
...value,
|
||||
authUrl: value.authUrl.replace(/\/$/, ""),
|
||||
auth: undefined,
|
||||
};
|
||||
|
||||
if (signer) {
|
||||
context.auth = new ThirdwebAuth(new SignerWallet(signer), value.domain);
|
||||
}
|
||||
|
||||
return context;
|
||||
}, [value, signer]);
|
||||
return (
|
||||
<ThirdwebAuthContext.Provider value={authContext}>
|
||||
{children}
|
||||
</ThirdwebAuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useThirdwebAuthContext() {
|
||||
return useContext(ThirdwebAuthContext);
|
||||
}
|
||||
+1
-1
@@ -1,10 +1,10 @@
|
||||
import {
|
||||
Chain,
|
||||
SupportedChainId,
|
||||
defaultSupportedChains,
|
||||
} from "../constants/chain";
|
||||
import { DEFAULT_RPC_URLS } from "@thirdweb-dev/sdk";
|
||||
import React, { PropsWithChildren, createContext, useContext } from "react";
|
||||
import { Chain } from "../types";
|
||||
|
||||
interface ThirdwebConfigContext {
|
||||
rpcUrlMap: Record<SupportedChainId | number, string>;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user