Preparing Major Version Bump (#1630)

This commit is contained in:
Jonas Daniels
2023-10-09 19:09:51 +00:00
committed by GitHub
parent 125c836d88
commit ce4608bff4
180 changed files with 351 additions and 11917 deletions
+16
View File
@@ -0,0 +1,16 @@
---
"@thirdweb-dev/react-core": major
"@thirdweb-dev/storage": major
"@thirdweb-dev/wallets": major
"@thirdweb-dev/react": major
"@thirdweb-dev/auth": major
"@thirdweb-dev/sdk": major
"thirdweb": minor
---
**Dropped Support for Solana**:
- We've decided to drop support for Solana across our SDKs.
- Starting with this version of our SDKs all previous solana related functionality will be removed.
- Previous versions of our SDKs will continue to work with Solana. However, we will not be providing any further updates or bug fixes for Solana.
- You can read a detailed explanation of our decision [on our blog](https://blog.thirdweb.com/discontinuing-solana-support/).
+13
View File
@@ -0,0 +1,13 @@
---
"@thirdweb-dev/storage": major
"@thirdweb-dev/wallets": major
"@thirdweb-dev/sdk": major
"thirdweb": minor
---
**Dropped Support for Node 16**:
- As part of our commitment to maintain up-to-date with the evolving ecosystem and provide the best support for our users, we have made the decision to cease support for Node 16. Starting with this version, our SDKs will no longer be tested against or guaranteed to work with Node.js version 16.
- We encourage users to upgrade to a more recent version of Node.js to continue to benefit from the latest features, improvements, and security updates.
- If you are unable to upgrade to a more recent version of Node.js, you can continue to use the previous version of our SDKs. However, we will not be providing any further updates or bug fixes for Node 16.
- For more information on EOL status of Node 16, see [Node.js Release Cycle](https://nodejs.dev/en/about/releases/).
+1 -1
View File
@@ -10,7 +10,7 @@ runs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
registry-url: "https://registry.npmjs.org"
cache: "pnpm"
-1
View File
@@ -29,7 +29,6 @@ You can see a quick outline of each of the projects within this repo below, each
| [/auth](./packages/auth) | Best in class wallet authentication for Node backends | <a href="https://www.npmjs.com/package/@thirdweb-dev/auth"><img src="https://img.shields.io/npm/v/@thirdweb-dev/auth?color=red&label=npm&logo=npm" alt="npm version"/></a> |
| [/storage](./packages/storage) | Best in class decentralized storage SDK for Browser and Node | <a href="https://www.npmjs.com/package/@thirdweb-dev/storage"><img src="https://img.shields.io/npm/v/@thirdweb-dev/storage?color=red&label=npm&logo=npm" alt="npm version"/></a> |
| [/cli](./packages/cli) | Publish and deploy smart contracts without dealing with private keys | <a href="https://www.npmjs.com/package/thirdweb"><img src="https://img.shields.io/npm/v/thirdweb?color=red&label=npm&logo=npm" alt="npm version"/></a> |
| [/solana](./packages/solana) | Solana SDK for Browser, Node and React Native | <a href="https://www.npmjs.com/package/@thirdweb-dev/solana"><img src="https://img.shields.io/npm/v/@thirdweb-dev/solana?color=red&label=npm&logo=npm" alt="npm version"/></a> |
## How to contribute
+4 -4
View File
@@ -56,7 +56,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
check-latest: true
- uses: pnpm/action-setup@v2
@@ -81,7 +81,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
check-latest: true
- uses: pnpm/action-setup@v2
@@ -100,7 +100,7 @@ jobs:
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
with:
files: ./packages/sdk/coverage/evm/lcov.info,./packages/sdk/coverage/solana/lcov.info,
files: ./packages/sdk/coverage/evm/lcov.info
e2e:
timeout-minutes: 15
@@ -111,7 +111,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
check-latest: true
- uses: pnpm/action-setup@v2
+42
View File
@@ -0,0 +1,42 @@
name: Release Next
on:
push:
paths:
- ".changeset/**"
- "packages/**"
branches:
- next
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Release
timeout-minutes: 30
runs-on: ubuntu-latest-16
steps:
- name: Checkout branch
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install
uses: ./.github/composite-actions/install
- name: Build
run: pnpm build
- name: Create @next release
run: |
git checkout next
pnpm version-packages:next
pnpm release:next
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -1 +1 @@
16
20
+5 -5
View File
@@ -7,11 +7,11 @@
"license": "MIT",
"private": true,
"engines": {
"node": ">=16.7.0"
"node": ">=18.0.0"
},
"scripts": {
"preinstall": "npx only-allow pnpm",
"test": "turbo run test test:evm test:solana --filter=./packages/*",
"test": "turbo run test --filter=./packages/*",
"e2e": "turbo run e2e",
"build": "turbo run build --filter=./packages/*",
"build:release": "turbo run build --force --filter=./packages/*",
@@ -26,12 +26,14 @@
"release": "node ./scripts/pre-release.mjs && changeset publish",
"version-packages:nightly": "node scripts/pre-nightly.mjs && changeset version --snapshot nightly",
"release:nightly": "node ./scripts/pre-release.mjs && changeset publish --tag nightly",
"version-packages:next": "node scripts/pre-nightly.mjs && changeset version --snapshot next",
"release:next": "node ./scripts/pre-release.mjs && changeset publish --tag next",
"push": "turbo run push --filter=./packages/*",
"hotlink-init": "node ./scripts/hotlink/hotlink-init.mjs",
"hotlink-revert": "node ./scripts/hotlink/hotlink-revert.mjs",
"explain:major": "node ./scripts/changeset-explain-major.mjs"
},
"packageManager": "pnpm@8.6.12",
"packageManager": "pnpm@8.9.0",
"dependencies": {
"@changesets/changelog-github": "^0.4.8",
"@changesets/cli": "^2.26.2",
@@ -54,10 +56,8 @@
},
"pnpm": {
"overrides": {
"cross-fetch@>=3.0.0 <3.1.5": ">=3.1.5",
"got@<11.8.5": ">=11.8.5",
"glob-parent@<5.1.2": ">=5.1.2",
"node-fetch@<2.6.7": ">=2.6.7",
"nth-check@<2.0.1": ">=2.0.1",
"socket.io-parser@>=4.0.4 <4.2.3": ">=4.2.3",
"fast-xml-parser@<4.2.4": ">=4.2.4",
+1 -1
View File
@@ -603,7 +603,7 @@
## 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.
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` entrypoint.
```js
import { PrivateKeyWallet } from "@thirdweb-dev/auth/evm";
+1 -14
View File
@@ -28,13 +28,6 @@
},
"default": "./next/dist/thirdweb-dev-auth-next.cjs.js"
},
"./solana": {
"module": {
"browser": "./solana/dist/thirdweb-dev-auth-solana.browser.esm.js",
"default": "./solana/dist/thirdweb-dev-auth-solana.esm.js"
},
"default": "./solana/dist/thirdweb-dev-auth-solana.cjs.js"
},
"./express": {
"module": {
"browser": "./express/dist/thirdweb-dev-auth-express.browser.esm.js",
@@ -70,8 +63,7 @@
"express/index.ts",
"next/index.ts",
"next-auth/index.ts",
"evm/index.ts",
"solana/index.ts"
"evm/index.ts"
],
"exports": {
"envConditions": [
@@ -87,7 +79,6 @@
"@microsoft/api-extractor": "^7.36.3",
"@microsoft/tsdoc": "^0.14.1",
"@preconstruct/cli": "2.7.0",
"@solana/web3.js": "^1.62.0",
"@swc-node/register": "^1.6.6",
"@swc/core": "^1.3.71",
"@thirdweb-dev/tsconfig": "workspace:*",
@@ -117,7 +108,6 @@
"typescript": "^5.1.6"
},
"peerDependencies": {
"@solana/web3.js": "^1.73.0",
"cookie-parser": "^1.4.6",
"ethers": "^5",
"express": "^4",
@@ -125,9 +115,6 @@
"next-auth": "^4"
},
"peerDependenciesMeta": {
"@solana/web3.js": {
"optional": true
},
"cookie-parser": {
"optional": true
},
-7
View File
@@ -1,7 +0,0 @@
{
"main": "dist/thirdweb-dev-auth-solana.cjs.js",
"module": "dist/thirdweb-dev-auth-solana.esm.js",
"browser": {
"./dist/thirdweb-dev-auth-solana.esm.js": "./dist/thirdweb-dev-auth-solana.browser.esm.js"
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ import { VerifyOptionsSchema } from "../schema/verify";
* Create an EIP-4361 & CAIP-122 compliant message to sign based on the login payload
*/
function createLoginMessage(payload: LoginPayloadData): string {
const typeField = payload.type === "evm" ? "Ethereum" : "Solana";
const typeField = "Ethereum";
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");
+1 -4
View File
@@ -23,10 +23,7 @@ export const RawDateSchema = z.date().transform((i) => {
return Math.floor(i.getTime() / 1000);
});
export const AccountTypeSchema = z.union([
z.literal("evm"),
z.literal("solana"),
]);
export const AccountTypeSchema = z.literal("evm");
export type User<TContext extends Json = Json> = {
address: string;
-29
View File
@@ -1,29 +0,0 @@
import { ThirdwebAuth } from "../core";
import { LoginPayload, VerifyOptions } from "../core/schema";
import { Keypair } from "@solana/web3.js";
import { KeypairWallet } from "@thirdweb-dev/wallets/solana/wallets/keypair";
const authMap = new Map<string, ThirdwebAuth>();
let wallet: KeypairWallet;
export async function verifyLogin(
domain: string,
payload: LoginPayload,
options?: Omit<VerifyOptions, "domain">,
) {
wallet = wallet || new KeypairWallet(Keypair.generate());
let auth: ThirdwebAuth;
if (!authMap.has(domain)) {
auth = new ThirdwebAuth(wallet, domain);
authMap.set(domain, auth);
} else {
auth = authMap.get(domain) as ThirdwebAuth;
}
try {
const address = await auth.verify(payload, options);
return { address, error: undefined };
} catch (err: any) {
return { address: undefined, error: err.message };
}
}
-353
View File
@@ -1,353 +0,0 @@
import { ThirdwebAuth } from "../src/core";
import { Keypair } from "@solana/web3.js";
import { KeypairWallet } from "@thirdweb-dev/wallets/solana/wallets/keypair";
import { expect } from "chai";
describe("Wallet Authentication - Solana", 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(
`The expected issuer address '${await signerWallet.getAddress()}' did not 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
View File
@@ -3,7 +3,6 @@ export const CREATE_MESSAGES = {
projectName: "What is your project named?",
contractName: "What will be the name of your new smart contract?",
extensionName: "What will be the name of your new extension?",
chain: "Which blockchain do you want to use?",
framework: "What framework do you want to use?",
language: "What language do you want to use?",
contract: "What type of contract do you want to start from?",
-2
View File
@@ -19,8 +19,6 @@ describe("npx thirdweb create", () => {
await pressKey("enter");
await waitForText(CREATE_MESSAGES.projectName);
await pressKey("enter");
await waitForText(CREATE_MESSAGES.chain);
await pressKey("enter");
await waitForText(CREATE_MESSAGES.framework);
// select CRA
await pressKey("arrowDown");
+1 -2
View File
@@ -6,7 +6,7 @@
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
"engines": {
"node": ">=16.0.0"
"node": ">=18.0.0"
},
"devDependencies": {
"@gmrchk/cli-testing-library": "^0.1.2",
@@ -50,7 +50,6 @@
"got": "11.8.5",
"inquirer": "^8.2.3",
"js-yaml": "^4.1.0",
"node-fetch": "^3.3.2",
"open": "^8.4.0",
"prompts": "^2.4.2",
"rimraf": "^3.0.2",
-1
View File
@@ -270,7 +270,6 @@ const main = async () => {
"Explicitly tell the CLI to bootstrap the app using pnpm",
)
.option("--framework [name]", "The preferred framework.")
.option("--solana", "Initialize as a Solana project.")
.option("--evm", "Initialize as an Ethereum project.")
.option(
"-t, --template [name]",
+10 -38
View File
@@ -118,9 +118,6 @@ export async function twCreate(
framework = "pwa-vite";
}
if (options.solana) {
chain = "solana";
}
if (options.evm) {
chain = "evm";
}
@@ -251,21 +248,7 @@ export async function twCreate(
if (!options.template) {
if (projectType === "app" && !chain) {
const res = await prompts({
type: "select",
name: "chain",
message: CREATE_MESSAGES.chain,
choices: [
{ title: "EVM", value: "evm" },
{ title: "Solana", value: "solana" },
],
});
if (res.chain === "solana") {
chain = "solana";
} else {
chain = "evm";
}
chain = "evm";
}
if (projectType === "app" && !framework) {
@@ -273,25 +256,15 @@ export async function twCreate(
type: "select",
name: "framework",
message: CREATE_MESSAGES.framework,
choices:
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: "PWA Vite", value: "pwa-vite" },
{ title: "React Native", value: "react-native" },
{ title: "Node.js", value: "node" },
{ title: "Express", value: "express" },
],
choices: [
{ title: "Next.js", value: "next" },
{ title: "Create React App", value: "cra" },
{ title: "Vite", value: "vite" },
{ title: "PWA Vite", value: "pwa-vite" },
{ title: "React Native", value: "react-native" },
{ title: "Node.js", value: "node" },
{ title: "Express", value: "express" },
],
});
if (typeof res.framework === "string") {
@@ -512,7 +485,6 @@ export async function twCreate(
framework,
language,
template,
chain,
});
} else {
await createContractProject({
@@ -24,14 +24,12 @@ export async function createApp({
framework,
language,
template,
chain,
}: {
appPath: string;
packageManager: PackageManager;
framework?: string;
language?: string;
template?: string;
chain?: string;
}): Promise<void> {
let frameworkPath = "";
@@ -47,9 +45,7 @@ export async function createApp({
process.exit(1);
}
} else if (framework) {
frameworkPath = `${framework}-${language || "javascript"}-${
chain === "solana" ? "solana-" : ""
}starter`;
frameworkPath = `${framework}-${language || "javascript"}-starter`;
const found = await hasTemplate(frameworkPath);
if (!found) {
-1
View File
@@ -156,7 +156,6 @@ export const getDependenciesForPython = (
export const getLatestVersion = async (packageName: string): Promise<any> => {
try {
const fetch = (await import("node-fetch")).default;
const url = `https://registry.npmjs.org/${packageName}`;
const response = await fetch(url);
if (!response.ok) {
@@ -1,339 +0,0 @@
/**
* 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
// },
//
// . . .
}
}
}
+3 -25
View File
@@ -25,24 +25,15 @@
},
"default": "./evm/dist/thirdweb-dev-react-core-evm.cjs.js"
},
"./solana": {
"module": {
"browser": "./solana/dist/thirdweb-dev-react-core-solana.browser.esm.js",
"default": "./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/"
"evm/"
],
"preconstruct": {
"entrypoints": [
"index.ts",
"solana/index.ts",
"evm/index.ts"
],
"exports": {
@@ -57,10 +48,9 @@
"lint": "eslint src/",
"fix": "eslint src/ --fix",
"clean": "rm -rf dist/",
"generate-docs": "pnpm generate-docs:evm && pnpm generate-docs:solana && pnpm generate-snippets",
"generate-docs": "pnpm generate-docs:evm && pnpm 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",
"generate-snippets": "node ./scripts/generate-snippets-evm.mjs && node ./scripts/generate-feature-snippets-evm.mjs",
"build": "tsc && preconstruct build",
"push": "yalc push"
},
@@ -75,8 +65,6 @@
"@microsoft/api-extractor": "^7.36.3",
"@microsoft/tsdoc": "^0.14.1",
"@preconstruct/cli": "2.7.0",
"@solana/wallet-adapter-react": "^0.15.19",
"@solana/web3.js": "^1.62.0",
"@thirdweb-dev/auth": "workspace:*",
"@thirdweb-dev/tsconfig": "workspace:*",
"@types/js-cookie": "^3.0.3",
@@ -109,17 +97,7 @@
"tiny-invariant": "^1.2.0"
},
"peerDependencies": {
"@solana/wallet-adapter-react": "^0.15.19",
"@solana/web3.js": "^1.62.0",
"ethers": ">=5.5.1",
"react": ">=18.0.0"
},
"peerDependenciesMeta": {
"@solana/web3.js": {
"optional": true
},
"@solana/wallet-adapter-react": {
"optional": true
}
}
}
@@ -1,135 +0,0 @@
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),
);
-7
View File
@@ -1,7 +0,0 @@
{
"main": "dist/thirdweb-dev-react-core-solana.cjs.js",
"module": "dist/thirdweb-dev-react-core-solana.esm.js",
"browser": {
"./dist/thirdweb-dev-react-core-solana.esm.js": "./dist/thirdweb-dev-react-core-solana.browser.esm.js"
}
}
@@ -1,12 +1,6 @@
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__";
@@ -53,28 +47,3 @@ export function createEVMContractQueryKey<TKey extends QueryKey>(
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,
);
}
@@ -1,53 +0,0 @@
import { 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 {}
const ThirdwebAuthContext = /* @__PURE__ */ createContext<
ThirdwebAuthContext | undefined
>(undefined);
export const ThirdwebAuthProvider: React.FC<
PropsWithChildren<{ value?: ThirdwebAuthConfig }>
> = ({ value, children }) => {
// Remove trailing slash from URL if present
const authContext = useMemo(() => {
if (!value) {
return undefined;
}
const context: ThirdwebAuthContext = {
...value,
authUrl: value.authUrl?.replace(/\/$/, ""),
};
return context;
}, [value]);
return (
<ThirdwebAuthContext.Provider value={authContext}>
{children}
</ThirdwebAuthContext.Provider>
);
};
export function useThirdwebAuthContext() {
return useContext(ThirdwebAuthContext);
}
@@ -1,7 +0,0 @@
export { useLogin } from "./useLogin";
export type { LoginConfig } from "./useLogin";
export { useLogout } from "./useLogout";
export type { UserWithData } from "./useUser";
export { useUser } from "./useUser";
@@ -1,204 +0,0 @@
import { ensureTWPrefix } from "../../../core/query-utils/query-key";
import { useThirdwebAuthContext } from "../../contexts/thirdweb-auth";
import { useSDK } from "../../providers/base";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type {
LoginOptions,
LoginPayload,
LoginPayloadData,
} from "@thirdweb-dev/auth";
import { GenericAuthWallet } from "@thirdweb-dev/wallets";
import { SignerWallet } from "@thirdweb-dev/wallets/solana/wallets/signer";
import invariant from "tiny-invariant";
export interface LoginConfig {
/**
* The URL to redirect to on login.
*/
redirectTo?: string;
/**
* Function to run on error.
*/
onError?: (error: string) => void;
}
/**
* Hook to securely login to a backend with the connected wallet. The backend
* authentication URL must be configured on the ThirdwebProvider.
*
* @param config - Configuration for the login.
* @returns - A function to invoke to login with the connected wallet.
*
* @beta
*/
export function useLogin() {
const queryClient = useQueryClient();
const authConfig = useThirdwebAuthContext();
const signer = useSDK()?.wallet.getSigner();
const login = useMutation({
mutationFn: async () => {
invariant(
authConfig,
"Please specify an authConfig in the ThirdwebProvider",
);
invariant(signer, "You need a connected wallet to login.");
invariant(
authConfig.authUrl,
"Please specify an authUrl in the authConfig.",
);
const wallet = new SignerWallet(signer);
const address = await wallet.getAddress();
let res = await fetch(`${authConfig.authUrl}/payload`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ address }),
});
if (!res.ok) {
throw new Error(`Failed to get payload with status code ${res.status}`);
}
let payloadData: LoginPayloadData;
try {
({ payload: payloadData } = await res.json());
} catch {
throw new Error(`Failed to get payload`);
}
const payload = await doLoginWithPayload(wallet, payloadData);
res = await fetch(`${authConfig.authUrl}/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ payload }),
credentials: "include",
});
if (!res.ok) {
const data = await res.json();
if (data.error) {
throw new Error(data.error);
}
throw new Error(`Login request failed with status code ${res.status}`);
}
queryClient.invalidateQueries(ensureTWPrefix(["user"]));
},
});
return {
login: () => login.mutateAsync(),
isLoading: login.isLoading,
};
}
// login function extracted directly from auth
const isBrowser = () => typeof window !== "undefined";
export async function doLoginWithPayload(
wallet: GenericAuthWallet,
payload: LoginPayloadData,
): Promise<LoginPayload> {
const message = generateMessage(payload);
const signature = await wallet.signMessage(message);
return {
payload,
signature,
};
}
export async function doLogin(
wallet: GenericAuthWallet,
options?: LoginOptions,
): Promise<LoginPayload> {
let chainId: string | undefined = options?.chainId;
if (!chainId && wallet.getChainId) {
try {
chainId = (await wallet.getChainId()).toString();
} catch {
// ignore error
}
}
invariant(options?.domain, "Please specify a domain to login with.");
// generate a pseudo-random nonce if none is provided
const nonce =
options?.nonce || Math.floor(Math.random() * 1000000000).toString();
const payloadData: LoginPayloadData = {
type: wallet.type,
domain: options.domain,
address: await wallet.getAddress(),
statement:
options?.statement ||
"Please ensure that the domain above matches the URL of the current website.",
version: options?.version || "1",
uri: options?.uri || (isBrowser() ? window.location.origin : undefined),
chain_id: chainId,
nonce: options?.nonce || nonce,
issued_at: new Date().toISOString(),
expiration_time: new Date(
options?.expirationTime || Date.now() + 1000 * 60 * 5,
).toISOString(),
invalid_before: new Date(
options?.invalidBefore || Date.now(),
).toISOString(),
resources: options?.resources,
};
return doLoginWithPayload(wallet, payloadData);
}
// generate straight from auth
function 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");
}
@@ -1,38 +0,0 @@
import { ensureTWPrefix } from "../../../core/query-utils/query-key";
import { useThirdwebAuthContext } from "../../contexts/thirdweb-auth";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import invariant from "tiny-invariant";
/**
* Hook to logout the connected wallet from the backend.
* The backend logout URL must be configured on the ThirdwebProvider.
*
* @returns - A function to invoke to logout.
*
* @beta
*/
export function useLogout() {
const queryClient = useQueryClient();
const authConfig = useThirdwebAuthContext();
const logout = useMutation({
mutationFn: async () => {
invariant(
authConfig,
"Please specify an authConfig in the ThirdwebProvider",
);
invariant(
authConfig.authUrl,
"Please specify an authUrl in the authConfig.",
);
await fetch(`${authConfig.authUrl}/logout`, {
method: "POST",
});
queryClient.invalidateQueries(ensureTWPrefix(["user"]));
},
});
return { logout: logout.mutateAsync, isLoading: logout.isLoading };
}
@@ -1,52 +0,0 @@
import { ensureTWPrefix } from "../../../core/query-utils/query-key";
import { useThirdwebAuthContext } from "../../contexts/thirdweb-auth";
import { useQuery } from "@tanstack/react-query";
import type { Json, User } from "@thirdweb-dev/auth";
import invariant from "tiny-invariant";
export interface UserWithData<
TData extends Json = Json,
TContext extends Json = Json,
> extends User<TContext> {
data?: TData;
}
/**
* Hook to get the currently logged in user.
*
* @returns - The currently logged in user or null if not logged in, as well as a loading state.
*
* @beta
*/
export function useUser<
TData extends Json = Json,
TContext extends Json = Json,
>() {
const authConfig = useThirdwebAuthContext();
const { data: user, isLoading } = useQuery(
ensureTWPrefix(["user"]),
async () => {
invariant(
authConfig,
"Please specify an authConfig in the ThirdwebProvider",
);
invariant(
authConfig.authUrl,
"Please specify an authUrl in the authConfig.",
);
// We include credentials, so we can getUser even if API is on different URL
const res = await fetch(`${authConfig.authUrl}/user`, {
credentials: "include",
});
return (await res.json()) as UserWithData<TData, TContext>;
},
{
enabled: !!authConfig,
},
);
return { user, isLoggedIn: !!user, isLoading };
}
@@ -1,45 +0,0 @@
import { createSOLProgramQueryKey } from "../../../../core/query-utils/query-key";
import { RequiredParam } from "../../../../core/query-utils/required-param";
import { MintNFTParams } from "../../../types";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { NFTCollection } from "@thirdweb-dev/sdk/solana";
import invariant from "tiny-invariant";
/**
* Mint NFTs on your NFT program
* @param program - The NFT program to mint NFTs to
*
* @example
* ```jsx
* import { useProgram, useMintNFT } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { mutateAsync: mintNFT, isLoading, error } = useMintNFT(program);
*
* return (
* <button onClick={() => mintNFT({ metadata: { name: "First NFT" } })}>
* Mint
* </button>
* )
* }
* ```
*
* @public
*/
export function useMintNFT(program: RequiredParam<NFTCollection>) {
const queryClient = useQueryClient();
return useMutation(
async (data: MintNFTParams) => {
invariant(program, "program is required");
if (!data.to) {
return await program.mint(data.metadata);
}
return await program.mintTo(data.to, data.metadata);
},
{
onSettled: () =>
queryClient.invalidateQueries(createSOLProgramQueryKey(program)),
},
);
}
@@ -1,51 +0,0 @@
import { createSOLProgramQueryKey } from "../../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../../core/query-utils/required-param";
import { MintNFTSupplyParams } from "../../../types";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { NFTCollection } from "@thirdweb-dev/sdk/solana";
/**
* Mint additional supply for an NFT on your NFT program
* @param program - The NFT program to mint additional NFTs to
*
* @example
* ```jsx
* import { useProgram, useMintNFTSupply } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { mutateAsync: mintSupply, isLoading, error } = useMintNFTSupply(program);
*
* return (
* <button onClick={() => mintSupply({ nftAddress: "111...", amount: 10 })}>
* Mint Additional Supply
* </button>
* )
* }
* ```
*
* @public
*/
export function useMintNFTSupply(program: RequiredParam<NFTCollection>) {
const queryClient = useQueryClient();
return useMutation(
async (data: MintNFTSupplyParams) => {
requiredParamInvariant(program, "program is required");
if (!data.to) {
return await program.mintAdditionalSupply(data.nftAddress, data.amount);
}
return await program.mintAdditionalSupplyTo(
data.to,
data.nftAddress,
data.amount,
);
},
{
onSettled: () =>
queryClient.invalidateQueries(createSOLProgramQueryKey(program)),
},
);
}
@@ -1,42 +0,0 @@
import { createSOLProgramQueryKey } from "../../../../core/query-utils/query-key";
import { RequiredParam } from "../../../../core/query-utils/required-param";
import { useQuery } from "@tanstack/react-query";
import type { NFTDrop } from "@thirdweb-dev/sdk/solana";
import invariant from "tiny-invariant";
export function claimConditionsQuery(program: RequiredParam<NFTDrop>) {
return {
queryKey: createSOLProgramQueryKey(program, ["claimConditions"] as const),
queryFn: async () => {
invariant(program, "program is required");
return await program.claimConditions.get();
},
enabled: !!program,
};
}
/**
* Get the current claim conditions on an NFT Drop
* @param program - The NFT Drop program to get the claim conditions for
*
* @example
* ```jsx
* import { useProgram, useClaimConditions } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { data: claimConditions, isLoading } = useClaimConditions(program);
*
* return (
* <p>{claimConditions?.price.displayValue}</p>
* )
* }
* ```
*
* @public
*/
export function useClaimConditions(program: RequiredParam<NFTDrop>) {
return useQuery(claimConditionsQuery(program));
}
@@ -1,45 +0,0 @@
import { createSOLProgramQueryKey } from "../../../../core/query-utils/query-key";
import { RequiredParam } from "../../../../core/query-utils/required-param";
import { ClaimNFTParams } from "../../../types";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { NFTDrop } from "@thirdweb-dev/sdk/solana";
import invariant from "tiny-invariant";
/**
* Claim NFTs from an NFT Drop program
* @param program - The NFT Drop program instance to claim from
*
* @example
* ```jsx
* import { useProgram, useClaimNFT } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { mutateAsync: claim, isLoading, error } = useClaimNFT(program);
*
* return (
* <button onClick={() => claim({amount: 1})}>
* Claim
* </button>
* )
* }
* ```
*
* @public
*/
export function useClaimNFT(program: RequiredParam<NFTDrop>) {
const queryClient = useQueryClient();
return useMutation(
async (data: ClaimNFTParams) => {
invariant(program, "program is required");
if (!data.to) {
return await program.claim(data.amount);
}
return await program.claimTo(data.to, data.amount);
},
{
onSettled: () =>
queryClient.invalidateQueries(createSOLProgramQueryKey(program)),
},
);
}
@@ -1,46 +0,0 @@
import { createSOLProgramQueryKey } from "../../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../../core/query-utils/required-param";
import { useQuery } from "@tanstack/react-query";
import type { NFTDrop } from "@thirdweb-dev/sdk/solana";
export function dropTotalClaimedSupplyQuery(program: RequiredParam<NFTDrop>) {
return {
queryKey: createSOLProgramQueryKey(program, [
"totalClaimedSupply",
] as const),
queryFn: async () => {
requiredParamInvariant(program, "program is required");
return await program.totalClaimedSupply();
},
enabled: !!program,
};
}
/**
* Get the total claimed supply of NFTs on an NFT Drop
* @param program - The NFT Drop program to get the claimed supply on
*
* @example
* ```jsx
* import { useProgram, useDropTotalClaimedSupply } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { data: claimedSupply, isLoading } = useDropTotalClaimedSupply(program);
*
* return (
* <p>{claimedSupply}</p>
* )
* }
* ```
*
* @public
*/
export function useDropTotalClaimedSupply(program: RequiredParam<NFTDrop>) {
return useQuery(dropTotalClaimedSupplyQuery(program));
}
@@ -1,53 +0,0 @@
import { createSOLProgramQueryKey } from "../../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../../core/query-utils/required-param";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { NFTMetadataInput, UploadProgressEvent } from "@thirdweb-dev/sdk";
import type { NFTDrop } from "@thirdweb-dev/sdk/solana";
/**
* Lazy mint NFTs on an NFT Drop program
* @param program - The NFT Drop program instance to lazy mint on
*
* @example
* ```jsx
* import { useProgram, useLazyMintNFT } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { mutateAsync: lazyMint, isLoading, error } = useLazyMintNFT(program);
*
* return (
* <button onClick={() => lazyMint({ name: "My NFT", description: "..." })}>
* Claim
* </button>
* )
* }
* ```
*
* @public
*/
export function useLazyMint(
program: RequiredParam<NFTDrop>,
onProgress?: (progress: UploadProgressEvent) => void,
) {
const queryClient = useQueryClient();
return useMutation(
async (data: { metadatas: NFTMetadataInput[] }) => {
requiredParamInvariant(program, "program is required");
let options;
if (onProgress) {
options = {
onProgress,
};
}
return await program.lazyMint(data.metadatas, options);
},
{
onSettled: () =>
queryClient.invalidateQueries(createSOLProgramQueryKey(program)),
},
);
}
@@ -1,41 +0,0 @@
import { createSOLProgramQueryKey } from "../../../../core/query-utils/query-key";
import { RequiredParam } from "../../../../core/query-utils/required-param";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { NFTDrop, NFTDropConditionsInput } from "@thirdweb-dev/sdk/solana";
import invariant from "tiny-invariant";
/**
* Set Claim Conditions to an NFT Drop program
* @param program - The NFT Drop program to set claim conditions for
*
* @example
* ```jsx
* import { useProgram, useSetClaimConditions } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { mutateAsync: setClaimConditions, isLoading, error } = useSetClaimConditions(program);
*
* return (
* <button onClick={() => setClaimConditions(metadata)}>
* Set Claim Conditions
* </button>
* )
* }
* ```
*
* @public
*/
export function useSetClaimConditions(program: RequiredParam<NFTDrop>) {
const queryClient = useQueryClient();
return useMutation(
async (metadata: NFTDropConditionsInput) => {
invariant(program, "program is required");
return await program.claimConditions.set(metadata);
},
{
onSettled: () =>
queryClient.invalidateQueries(createSOLProgramQueryKey(program)),
},
);
}
@@ -1,46 +0,0 @@
import { createSOLProgramQueryKey } from "../../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../../core/query-utils/required-param";
import { useQuery } from "@tanstack/react-query";
import type { NFTDrop } from "@thirdweb-dev/sdk/solana";
export function dropUnclaimedSupplyQuery(program: RequiredParam<NFTDrop>) {
return {
queryKey: createSOLProgramQueryKey(program, [
"totalUnclaimedSupply",
] as const),
queryFn: async () => {
requiredParamInvariant(program, "program is required");
return await program.totalUnclaimedSupply();
},
enabled: !!program,
};
}
/**
* Get the total unclaimed supply of NFTs on an NFT Drop
* @param program - The NFT Drop program to get the unclaimed supply on
*
* @example
* ```jsx
* import { useProgram, useDropTotalUnclaimedSupply } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { data: unclaimedSupply, isLoading } = useDropTotalUnclaimedSupply(program);
*
* return (
* <p>{unclaimedSupply}</p>
* )
* }
* ```
*
* @public
*/
export function useDropUnclaimedSupply(program: RequiredParam<NFTDrop>) {
return useQuery(dropUnclaimedSupplyQuery(program));
}
@@ -1,45 +0,0 @@
import { createSOLProgramQueryKey } from "../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../core/query-utils/required-param";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { NFTCollection, NFTDrop } from "@thirdweb-dev/sdk/solana";
/**
* Burn an NFT owned by the connected wallet
* @param program - The NFT program instance to burn NFTs on
*
* @example
* ```jsx
* import { useProgram, useBurnNFT } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { mutateAsync: burn, isLoading, error } = useBurnNFT(program);
*
* return (
* <button
* onClick={() => burn("...")}
* >
* Burn
* </button>
* )
* }
* ```
*
* @public
*/
export function useBurnNFT(program: RequiredParam<NFTCollection | NFTDrop>) {
const queryClient = useQueryClient();
return useMutation(
async (nftAddress: string) => {
requiredParamInvariant(program, "program is required");
return await program.burn(nftAddress);
},
{
onSettled: () =>
queryClient.invalidateQueries(createSOLProgramQueryKey(program)),
},
);
}
@@ -1,45 +0,0 @@
import { createSOLProgramQueryKey } from "../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../core/query-utils/required-param";
import { useQuery } from "@tanstack/react-query";
import { NFTCollection, NFTDrop } from "@thirdweb-dev/sdk/solana";
export function nftCreatorsQuery(
program: RequiredParam<NFTCollection | NFTDrop>,
) {
return {
queryKey: createSOLProgramQueryKey(program, ["creators"] as const),
queryFn: async () => {
requiredParamInvariant(program, "program is required");
return await program.getCreators();
},
enabled: !!program,
};
}
/**
* Get the creators for an NFT program
* @param program - The NFT program to get the creators for
*
* @example
* ```jsx
* import { useProgram, useCreators } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { data: creators, isLoading } = useCreators(program);
*
* return (
* <pre>{JSON.stringify(creators)}</pre>
* )
* }
* ```
*
* @public
*/
export function useCreators(program: RequiredParam<NFTCollection | NFTDrop>) {
return useQuery(nftCreatorsQuery(program));
}
@@ -1,53 +0,0 @@
import { createSOLProgramQueryKey } from "../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../core/query-utils/required-param";
import { useQuery } from "@tanstack/react-query";
import type { NFTCollection, NFTDrop } from "@thirdweb-dev/sdk/solana";
export function nftGetOneQuery(
program: RequiredParam<NFTCollection | NFTDrop>,
tokenAddress: RequiredParam<string>,
) {
return {
queryKey: createSOLProgramQueryKey(program, [
"get",
{ tokenAddress },
] as const),
queryFn: async () => {
requiredParamInvariant(program, "program is required");
requiredParamInvariant(tokenAddress, "tokenAddress is required");
return await program.get(tokenAddress);
},
enabled: !!program && !!tokenAddress,
};
}
/**
* Get the metadata for a minted NFT
* @param program - The NFT program to get NFT metadata from
* @param tokenAddress - The mint address of the NFT to get the metadata of *
* @example
* ```jsx
* import { useProgram, useNFT } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { data: metadata, isLoading } = useNFT(program, mintAddress);
*
* return (
* <pre>{JSON.stringify(metadata)}</pre>
* )
* }
* ```
*
* @public
*/
export function useNFT(
program: RequiredParam<NFTCollection | NFTDrop>,
tokenAddress: RequiredParam<string>,
) {
return useQuery(nftGetOneQuery(program, tokenAddress));
}
@@ -1,53 +0,0 @@
import { createSOLProgramQueryKey } from "../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../core/query-utils/required-param";
import { useQuery } from "@tanstack/react-query";
import { QueryAllParams } from "@thirdweb-dev/sdk";
import type { NFTCollection, NFTDrop } from "@thirdweb-dev/sdk/solana";
export function nftGetAllQuery(
program: RequiredParam<NFTCollection | NFTDrop>,
queryParams?: QueryAllParams,
) {
return {
queryKey: createSOLProgramQueryKey(program, [
"getAll",
queryParams,
] as const),
queryFn: async () => {
requiredParamInvariant(program, "program is required");
return program.getAll(queryParams);
},
enabled: !!program,
};
}
/**
* Get the metadata for every NFT on an NFT program
* @param program - The NFT program to get NFTs metadata from
*
* @example
* ```jsx
* import { useProgram, useNFTs } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { data: metadata, isLoading } = useNFTs(program);
*
* return (
* <pre>{JSON.stringify(metadata)}</pre>
* )
* }
* ```
*
* @public
*/
export function useNFTs(
program: RequiredParam<NFTCollection | NFTDrop>,
queryParams?: QueryAllParams,
) {
return useQuery(nftGetAllQuery(program, queryParams));
}
@@ -1,47 +0,0 @@
import { createSOLProgramQueryKey } from "../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../core/query-utils/required-param";
import { useQuery } from "@tanstack/react-query";
import { NFTCollection, NFTDrop } from "@thirdweb-dev/sdk/solana";
export function nftRoyaltyQuery(
program: RequiredParam<NFTCollection | NFTDrop>,
) {
return {
queryKey: createSOLProgramQueryKey(program, ["royalty"] as const),
queryFn: async () => {
requiredParamInvariant(program, "program is required");
return await program.getRoyalty();
},
enabled: !!program,
};
}
/**
* Get the royalty for an NFT program
* @param program - The NFT program to get the royalty for
*
* @example
* ```jsx
* import { useProgram, useRoyalty } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { data: royalty, isLoading } = useRoyaltySettings(program);
*
* return (
* <p>{royalty}</p>
* )
* }
* ```
*
* @public
*/
export function useRoyaltySettings(
program: RequiredParam<NFTCollection | NFTDrop>,
) {
return useQuery(nftRoyaltyQuery(program));
}
@@ -1,47 +0,0 @@
import { createSOLProgramQueryKey } from "../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../core/query-utils/required-param";
import { useQuery } from "@tanstack/react-query";
import type { NFTCollection, NFTDrop } from "@thirdweb-dev/sdk/solana";
export function nftTotalSupplyQuery(
program: RequiredParam<NFTCollection | NFTDrop>,
) {
return {
queryKey: createSOLProgramQueryKey(program, ["supply"] as const),
queryFn: async () => {
requiredParamInvariant(program, "program is required");
return program.totalSupply();
},
enabled: !!program,
};
}
/**
* Get the total supply of NFTs on the program
* @param program - The NFT program to get the total supply of
*
* @example
* ```jsx
* import { useProgram, useTotalSupply } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { data: supply, isLoading } = useTotalSupply(program);
*
* return (
* <pre>{JSON.stringify(supply)}</pre>
* )
* }
* ```
*
* @public
*/
export function useTotalSupply(
program: RequiredParam<NFTCollection | NFTDrop>,
) {
return useQuery(nftTotalSupplyQuery(program));
}
@@ -1,55 +0,0 @@
import { createSOLProgramQueryKey } from "../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../core/query-utils/required-param";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { NFTCollection, NFTDrop } from "@thirdweb-dev/sdk/solana";
export type TransferNFTMutationParams = {
receiverAddress: string;
tokenAddress: string;
};
/**
* Transfer NFTs from the connected wallet to another wallet
* @param program - The NFT program instance to transfer NFTs on
*
* @example
* ```jsx
* import { useProgram, useTransferNFT } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { mutateAsync: transfer, isLoading, error } = useTransferNFT(program);
*
* return (
* <button
* onClick={() => transfer({
* receiverAddress: "{{wallet_address}}",
* tokenAddress: "..."
* })}
* >
* Transfer
* </button>
* )
* }
* ```
*
* @public
*/
export function useTransferNFT(
program: RequiredParam<NFTCollection | NFTDrop>,
) {
const queryClient = useQueryClient();
return useMutation(
async ({ tokenAddress, receiverAddress }: TransferNFTMutationParams) => {
requiredParamInvariant(program, "program is required");
return await program.transfer(receiverAddress, tokenAddress);
},
{
onSettled: () =>
queryClient.invalidateQueries(createSOLProgramQueryKey(program)),
},
);
}
@@ -1,51 +0,0 @@
import { createSOLProgramQueryKey } from "../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../core/query-utils/required-param";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type {
NFTCollection,
NFTDrop,
UpdateCreatorInput,
} from "@thirdweb-dev/sdk/solana";
/**
* Update the creators for an NFT program
* @param program - The NFT program instance to update the creators for
*
* @example
* ```jsx
* import { useProgram, useUpdateCreators } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { mutateAsync: updateCreators, isLoading, error } = useUpdateCreators(program);
*
* return (
* <button
* onClick={() => updateCreators([{ address: "0x...", share: 10 }])}
* >
* Update Creators
* </button>
* )
* }
* ```
*
* @public
*/
export function useUpdateCreators(
program: RequiredParam<NFTCollection | NFTDrop>,
) {
const queryClient = useQueryClient();
return useMutation(
async ({ creators, updateAll }: UpdateCreatorInput) => {
requiredParamInvariant(program, "program is required");
return await program.updateCreators(creators, updateAll);
},
{
onSettled: () =>
queryClient.invalidateQueries(createSOLProgramQueryKey(program)),
},
);
}
@@ -1,51 +0,0 @@
import { createSOLProgramQueryKey } from "../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../core/query-utils/required-param";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type {
NFTCollection,
NFTDrop,
UpdateRoyaltySettingsInput,
} from "@thirdweb-dev/sdk/solana";
/**
* Update the royalty for an NFT program
* @param program - The NFT program instance to update the royalty for
*
* @example
* ```jsx
* import { useProgram, useUpdateRoyaltySettings } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { mutateAsync: updateRoyalties, isLoading, error } = useUpdateRoyaltySettings(program);
*
* return (
* <button
* onClick={() => updateRoyalties(300)}
* >
* Update Royalties
* </button>
* )
* }
* ```
*
* @public
*/
export function useUpdateRoyaltySettings(
program: RequiredParam<NFTCollection | NFTDrop>,
) {
const queryClient = useQueryClient();
return useMutation(
async ({ sellerFeeBasisPoints, updateAll }: UpdateRoyaltySettingsInput) => {
requiredParamInvariant(program, "program is required");
return await program.updateRoyalty(sellerFeeBasisPoints, updateAll);
},
{
onSettled: () =>
queryClient.invalidateQueries(createSOLProgramQueryKey(program)),
},
);
}
@@ -1,127 +0,0 @@
import {
createSOLQueryKeyWithNetwork,
neverPersist,
} from "../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../core/query-utils/required-param";
import { useSDK } from "../../providers/base";
import { programAccountTypeQuery } from "./useProgramAccountType";
import {
QueryClient,
useQuery,
useQueryClient,
UseQueryResult,
} from "@tanstack/react-query";
import type {
NFTCollection,
NFTDrop,
ThirdwebSDK,
Token,
} from "@thirdweb-dev/sdk/solana";
type ProgramMap = Readonly<{
"nft-collection": NFTCollection;
"nft-drop": NFTDrop;
token: Token;
}>;
type ProgramType = keyof ProgramMap;
export function programQuery<TProgramType extends ProgramType>(
queryClient: QueryClient,
sdk: RequiredParam<ThirdwebSDK>,
address: RequiredParam<string>,
type?: TProgramType,
) {
const network = sdk?.network;
return {
queryKey: neverPersist(
createSOLQueryKeyWithNetwork(
["program-instance", { address, type }] as const,
network || null,
),
),
queryFn: (async () => {
requiredParamInvariant(sdk, "sdk is required");
requiredParamInvariant(address, "Address is required");
// if the type is not passed in explicitly then we'll try to resolve it
if (!type) {
// why do we call `fetchQuery` here instead of calling the sdk directly?
// while we can never persist the program itself to the cache we can persist the type!
// (and this will be triggered by fetching the query on the queryClient)
type = await queryClient.fetchQuery(
programAccountTypeQuery(sdk, address),
);
}
switch (type) {
case "nft-collection":
return await sdk.getNFTCollection(address);
case "nft-drop":
return await sdk.getNFTDrop(address);
case "token":
return await sdk.getToken(address);
default:
throw new Error("Unknown account type");
}
// this is the magic that makes the type inference work
}) as () => Promise<ProgramMap[TProgramType]>,
enabled: !!sdk && !!network && !!address,
// this cannot change as it is unique by address & network
cacheTime: Infinity,
staleTime: Infinity,
};
}
/**
* Get an SDK instance to interact with any program
* @param address - the address of the program to get an interface for
* @param type - optionally, pass in the program type to get static typing
*
* @example
* ```jsx
* import { useProgram } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}").program;
*
* // Now you can use the program in the rest of the component
*
* // For example, we can make a transaction
* async function functionCall() {
* await program.call("mint", ...);
* }
*
* ...
* }
* ```
*
* @public
*/
export function useProgram<TProgramType extends ProgramType>(
address: RequiredParam<string>,
type?: TProgramType,
) {
const queryClient = useQueryClient();
const sdk = useSDK();
const queryResult = useQuery(programQuery(queryClient, sdk, address, type));
return { ...queryResult, program: queryResult.data } as UseQueryResult<
Readonly<{
"nft-collection": NFTCollection;
"nft-drop": NFTDrop;
token: Token;
}>[TProgramType],
unknown
> & {
program: Readonly<{
"nft-collection": NFTCollection;
"nft-drop": NFTDrop;
token: Token;
}>[TProgramType];
};
}
export type UseProgramResult = ReturnType<typeof useProgram>;
@@ -1,39 +0,0 @@
import { createSOLQueryKeyWithNetwork } from "../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../core/query-utils/required-param";
import { useSDK } from "../../providers/base";
import { useQuery } from "@tanstack/react-query";
import type { ThirdwebSDK } from "@thirdweb-dev/sdk/solana";
export function programAccountTypeQuery(
sdk: RequiredParam<ThirdwebSDK>,
address: RequiredParam<string>,
) {
const network = sdk?.network;
return {
queryKey: createSOLQueryKeyWithNetwork(
["program", address, "type"] as const,
network || null,
),
queryFn: async () => {
requiredParamInvariant(sdk, "sdk is required");
requiredParamInvariant(address, "Address is required");
return await sdk.registry.getProgramType(address);
},
enabled: !!sdk && !!network && !!address,
// this cannot change as it is unique by address & network
cacheTime: Infinity,
staleTime: Infinity,
};
}
/**
* @internal
*/
export function useProgramAccountType(address: RequiredParam<string>) {
const sdk = useSDK();
return useQuery(programAccountTypeQuery(sdk, address));
}
@@ -1,33 +0,0 @@
import { createSOLProgramQueryKey } from "../../../core/query-utils/query-key";
import { RequiredParam } from "../../../core/query-utils/required-param";
import { UseProgramResult } from "./useProgram";
import { useQuery, UseQueryResult } from "@tanstack/react-query";
import { NFTMetadata } from "@thirdweb-dev/sdk";
import invariant from "tiny-invariant";
export function programMetadataQuery(
program: RequiredParam<UseProgramResult["program"]>,
): {
queryKey: ReturnType<typeof createSOLProgramQueryKey>;
queryFn(): Promise<NFTMetadata>;
enabled: boolean;
} {
return {
queryKey: createSOLProgramQueryKey(program || null, ["metadata"] as const),
queryFn: async () => {
invariant(program, "sdk is required");
return (await program.getMetadata()) as NFTMetadata;
},
enabled: !!program,
};
}
/**
* @internal
*/
export function useProgramMetadata(
program: RequiredParam<UseProgramResult["data"]>,
): UseQueryResult<NFTMetadata> {
return useQuery(programMetadataQuery(program));
}
@@ -1,45 +0,0 @@
import { createSOLProgramQueryKey } from "../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../core/query-utils/required-param";
import type { TokenParams } from "../../../evm";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { Token } from "@thirdweb-dev/sdk/solana";
/**
* Mint tokens on your token program
* @param program - The program instance of the program to mint on
*
* @example
* ```jsx
* import { useProgram, useMintToken } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { mutateAsync: mint, isLoading, error } = useMintToken(program);
*
* return (
* <button onClick={() => mint({ to: "{{wallet_address}}", amount: 1 })}>
* Mint
* </button>
* )
* }
* ```
*
* @public
*/
export function useMintToken(program: RequiredParam<Token>) {
const queryClient = useQueryClient();
return useMutation(
async (params: TokenParams) => {
requiredParamInvariant(program, "program is required");
// TODO switch this to use mintTo once that is exposed
return await program.mint(params.amount);
},
{
onSettled: () =>
queryClient.invalidateQueries(createSOLProgramQueryKey(program)),
},
);
}
@@ -1,53 +0,0 @@
import { createSOLProgramQueryKey } from "../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../core/query-utils/required-param";
import { useQuery } from "@tanstack/react-query";
import { Token } from "@thirdweb-dev/sdk/solana";
export function tokenBalanceQuery(
program: RequiredParam<Token>,
walletAddress: RequiredParam<string>,
) {
return {
queryKey: createSOLProgramQueryKey(program, [
"balanceOf",
{ walletAddress },
] as const),
queryFn: async () => {
requiredParamInvariant(program, "program is required");
requiredParamInvariant(walletAddress, "Wallet address is required");
return await program.balanceOf(walletAddress);
},
enabled: !!program && !!walletAddress,
};
}
/**
* Get the token balance of a specified wallet
* @param program - The token program to get the balance on
* @param walletAddress - The address of the wallet to get the balance of
*
* @example
* ```jsx
* import { useProgram, useTokenBalance } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { data: balance, isLoading } = useTokenBalance(program, "{{wallet_address}}");
*
* return (
* <p>{balance}</p>
* )
* }
* ```
*
* @public
*/
export function useTokenBalance(
program: RequiredParam<Token>,
walletAddress: RequiredParam<string>,
) {
return useQuery(tokenBalanceQuery(program, walletAddress));
}
@@ -1,41 +0,0 @@
import { createSOLProgramQueryKey } from "../../../core/query-utils/query-key";
import { RequiredParam } from "../../../core/query-utils/required-param";
import { useQuery } from "@tanstack/react-query";
import { Token } from "@thirdweb-dev/sdk/solana";
import invariant from "tiny-invariant";
export function tokenSupplyQuery(program: RequiredParam<Token>) {
return {
queryKey: createSOLProgramQueryKey(program, ["totalSupply"] as const),
queryFn: async () => {
invariant(program, "program is required");
return await program.totalSupply();
},
enabled: !!program,
};
}
/**
* Get the total circulating supply of a token
* @param program - The token program to get the supply of
*
* @example
* ```jsx
* import { useProgram, useMintToken } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { data: totalSupply, isLoading } = useTokenSupply(program);
*
* return (
* <p>{totalSupply}</p>
* )
* }
* ```
*
* @public
*/
export function useTokenSupply(program: RequiredParam<Token>) {
return useQuery(tokenSupplyQuery(program));
}
@@ -1,49 +0,0 @@
import { createSOLProgramQueryKey } from "../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../core/query-utils/required-param";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { Amount } from "@thirdweb-dev/sdk";
import type { Token } from "@thirdweb-dev/sdk/solana";
export type TransferTokenMutationParams = {
amount: Amount;
receiverAddress: string;
};
/**
* Transfer tokens from the connected wallet to another wallet
* @param program - The program instance of the program to mint on
*
* @example
* ```jsx
* import { useProgram, useTransferToken } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { mutateAsync: transfer, isLoading, error } = useTransferToken(program);
*
* return (
* <button onClick={() => transfer({ to: "{{wallet_address}}", amount: 1 })}>
* Transfer
* </button>
* )
* }
* ```
*
* @public
*/
export function useTransferToken(program: RequiredParam<Token>) {
const queryClient = useQueryClient();
return useMutation(
async ({ amount, receiverAddress }: TransferTokenMutationParams) => {
requiredParamInvariant(program, "program is required");
return await program.transfer(receiverAddress, amount);
},
{
onSettled: () =>
queryClient.invalidateQueries(createSOLProgramQueryKey(program)),
},
);
}
@@ -1,35 +0,0 @@
import { createSOLQueryKeyWithNetwork } from "../../../core/query-utils/query-key";
import { RequiredParam } from "../../../core/query-utils/required-param";
import { useSDK } from "../../providers/base";
import { useQuery } from "@tanstack/react-query";
import { ThirdwebSDK } from "@thirdweb-dev/sdk/solana";
import invariant from "tiny-invariant";
export function balanceQuery(sdk: RequiredParam<ThirdwebSDK>) {
const address = sdk?.wallet?.getAddress();
const network = sdk?.network;
return {
queryKey: createSOLQueryKeyWithNetwork(
["wallet-balance", { address }] as const,
network,
),
queryFn: () => {
invariant(sdk, "sdk is required");
return sdk.wallet.getBalance();
},
enabled: !!sdk && !!address && !!network,
};
}
/**
* Get the currently connected wallet balance
*
* @returns the balance of the connected wallet
*/
export function useBalance() {
const sdk = useSDK();
return useQuery(balanceQuery(sdk));
}
-86
View File
@@ -1,86 +0,0 @@
// Providers
export { ThirdwebSDKProvider, useSDK } from "./providers/base";
/**
* Hooks
*/
// Program
export { programQuery, useProgram } from "./hooks/program/useProgram";
export type { UseProgramResult } from "./hooks/program/useProgram";
export {
programMetadataQuery,
useProgramMetadata,
} from "./hooks/program/useProgramMetadata";
export {
programAccountTypeQuery,
useProgramAccountType,
} from "./hooks/program/useProgramAccountType";
// NFT Shared
export { nftGetAllQuery, useNFTs } from "./hooks/nft/useNFTs";
export { useTransferNFT } from "./hooks/nft/useTransferNFT";
export type { TransferNFTMutationParams } from "./hooks/nft/useTransferNFT";
export { useBurnNFT } from "./hooks/nft/useBurnNFT";
export {
nftRoyaltyQuery,
useRoyaltySettings,
} from "./hooks/nft/useRoyaltySettings";
export { useUpdateRoyaltySettings } from "./hooks/nft/useUpdateRoyaltySettings";
export { nftCreatorsQuery, useCreators } from "./hooks/nft/useCreators";
export { useUpdateCreators } from "./hooks/nft/useUpdateCreators";
export {
nftTotalSupplyQuery,
useTotalSupply,
} from "./hooks/nft/useTotalSupply";
// NFT Collection
export { useMintNFT } from "./hooks/nft/collection/useMintNFT";
export { useMintNFTSupply } from "./hooks/nft/collection/useMintNFTSupply";
// NFT Drop
export { useLazyMint } from "./hooks/nft/drop/useLazyMint";
export {
dropTotalClaimedSupplyQuery,
useDropTotalClaimedSupply,
} from "./hooks/nft/drop/useClaimedSupply";
export {
dropUnclaimedSupplyQuery,
useDropUnclaimedSupply,
} from "./hooks/nft/drop/useUnclaimedSupply";
export { useClaimNFT } from "./hooks/nft/drop/useClaimNFT";
export {
claimConditionsQuery,
useClaimConditions,
} from "./hooks/nft/drop/useClaimConditions";
export { useSetClaimConditions } from "./hooks/nft/drop/useSetClaimConditions";
// Token
export { tokenSupplyQuery, useTokenSupply } from "./hooks/token/useTokenSupply";
export {
tokenBalanceQuery,
useTokenBalance,
} from "./hooks/token/useTokenBalance";
export { useMintToken } from "./hooks/token/useMintToken";
export type { TransferTokenMutationParams } from "./hooks/token/useTransferToken";
export { useTransferToken } from "./hooks/token/useTransferToken";
// Auth
export { useLogin, useLogout, useUser } from "./hooks/auth";
export type { UserWithData, LoginConfig } from "./hooks/auth";
//Wallet
export { balanceQuery, useBalance } from "./hooks/wallet/useBalance";
//Contexts
export type { ThirdwebAuthConfig } from "./contexts/thirdweb-auth";
export {
ThirdwebAuthProvider,
useThirdwebAuthContext,
} from "./contexts/thirdweb-auth";
// Utilities and Others
export { shouldNeverPersistQuery } from "../core/query-utils/query-key";
export type { RequiredParam } from "../core/query-utils/required-param";
@@ -1,115 +0,0 @@
import {
QueryClientProviderProps,
QueryClientProviderWithDefault,
} from "../../core/providers/query-client";
import { RequiredParam } from "../../core/query-utils/required-param";
import { ComponentWithChildren } from "../../core/types/component";
import {
ThirdwebAuthConfig,
ThirdwebAuthProvider,
} from "../contexts/thirdweb-auth";
import type { WalletContextState } from "@solana/wallet-adapter-react";
import { Network, ThirdwebSDK } from "@thirdweb-dev/sdk/solana";
import { createContext, useContext, useEffect, useMemo, useState } from "react";
import invariant from "tiny-invariant";
interface ThirdwebSDKProviderProps extends QueryClientProviderProps {
network: RequiredParam<Network>;
wallet?: WalletContextState;
authConfig?: ThirdwebAuthConfig;
}
/**
* Gives access to the ThirdwebSDK instance and other useful hooks to the rest of the app.
* Requires to be wrapped with a ConnectionProvider and a WalletProvider from @solana/wallet-adapter-react.
* @example
* ```tsx
* import { useWallet } from "@solana/wallet-adapter-react";
* import { ThirdwebProvider } from "@thirdweb-dev/react/solana";
*
* const ThirdwebApp = () => {
* const wallet = useWallet();
* return (
* <ThirdwebSDKProvider network={"devnet"} wallet={wallet}>
* <YourApp />
* </ThirdwebSDKProvider>
* )};
* ```
*/
export const ThirdwebSDKProvider: ComponentWithChildren<
ThirdwebSDKProviderProps
> = ({ children, network, queryClient, wallet, authConfig }) => {
const [sdk, setSDK] = useState<ThirdwebSDK | null>(null);
useEffect(() => {
if (network) {
const _sdk = ThirdwebSDK.fromNetwork(network);
if (wallet && wallet.publicKey) {
_sdk.wallet.connect(wallet);
}
(_sdk as any)._network = network;
setSDK(_sdk);
} else {
setSDK(null);
}
// disabled wallet on purpose because we handle that below
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [network]);
useEffect(() => {
if (
wallet &&
wallet.publicKey &&
sdk &&
(sdk as any)._network === network
) {
sdk.wallet.connect(wallet);
return;
}
}, [network, sdk, wallet]);
const ctxValue = useMemo(
() =>
({
sdk,
desiredNetwork: network || "unknown",
_inProvider: true,
}) as const,
[sdk, network],
);
return (
<QueryClientProviderWithDefault queryClient={queryClient}>
<ThirdwebAuthProvider value={authConfig}>
<ThirdwebSDKContext.Provider value={ctxValue}>
{children}
</ThirdwebSDKContext.Provider>
</ThirdwebAuthProvider>
</QueryClientProviderWithDefault>
);
};
interface ThirdwebSDKContext {
sdk: ThirdwebSDK | null;
desiredNetwork: string;
_inProvider?: true;
}
const ThirdwebSDKContext = /* @__PURE__ */ createContext<ThirdwebSDKContext>({
sdk: null,
desiredNetwork: "unknown",
});
export function useSDK() {
const ctxValue = useContext(ThirdwebSDKContext);
invariant(
ctxValue._inProvider,
"useSDK must be used within a ThirdwebSDKProvider",
);
if (
!ctxValue.sdk ||
(ctxValue.sdk as any)._network !== ctxValue.desiredNetwork
) {
return null;
}
return ctxValue.sdk;
}
-32
View File
@@ -1,32 +0,0 @@
import { Amount, NFTMetadataOrUri } from "@thirdweb-dev/sdk";
/**
* The params for the {@link useMintNFT} hook mutation.
*
* @beta
*/
export type MintNFTParams = {
to?: string;
metadata: NFTMetadataOrUri;
amount?: Amount;
};
/**
* The params for the {@link useClaimNFT} hook mutation.
*
* @beta
*/
export type ClaimNFTParams = {
to?: string;
amount: Amount;
};
/**
* The params to pass to {@link useMintNFTSupply} hook mutation
* @beta
*/
export type MintNFTSupplyParams = {
to?: string;
nftAddress: string;
amount: Amount;
};
@@ -1,342 +0,0 @@
/**
* 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-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": [
"wagmi",
"wagmi-core"
],
/**
* 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
// },
//
// . . .
}
}
}
+3 -40
View File
@@ -25,13 +25,6 @@
},
"default": "./evm/dist/thirdweb-dev-react-evm.cjs.js"
},
"./solana": {
"module": {
"browser": "./solana/dist/thirdweb-dev-react-solana.browser.esm.js",
"default": "./solana/dist/thirdweb-dev-react-solana.esm.js"
},
"default": "./solana/dist/thirdweb-dev-react-solana.cjs.js"
},
"./evm/connectors/magic": {
"module": {
"browser": "./evm/connectors/magic/dist/thirdweb-dev-react-evm-connectors-magic.browser.esm.js",
@@ -50,13 +43,11 @@
},
"files": [
"dist/",
"evm/",
"solana/"
"evm/"
],
"preconstruct": {
"entrypoints": [
"index.ts",
"solana/index.ts",
"evm/index.ts",
"evm/connectors/**/index.ts"
],
@@ -72,10 +63,9 @@
"lint": "eslint src/",
"fix": "eslint src/ --fix",
"clean": "rm -rf dist/",
"generate-docs": "pnpm generate-docs:evm && pnpm generate-docs:solana && pnpm generate-snippets",
"generate-docs": "pnpm generate-docs:evm && pnpm 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",
"generate-snippets": "node ./scripts/generate-snippets-evm.mjs && node ./scripts/generate-feature-snippets-evm.mjs",
"build": "tsc && preconstruct build",
"push": "yalc push"
},
@@ -90,11 +80,6 @@
"@microsoft/api-extractor": "^7.36.3",
"@microsoft/tsdoc": "^0.14.1",
"@preconstruct/cli": "2.7.0",
"@solana/wallet-adapter-base": "^0.9.17",
"@solana/wallet-adapter-phantom": "^0.9.17",
"@solana/wallet-adapter-react": "^0.15.19",
"@solana/wallet-adapter-wallets": "^0.19.0",
"@solana/web3.js": "^1.62.0",
"@thirdweb-dev/auth": "workspace:*",
"@thirdweb-dev/sdk": "workspace:*",
"@thirdweb-dev/tsconfig": "workspace:*",
@@ -144,31 +129,9 @@
"tiny-invariant": "^1.2.0"
},
"peerDependencies": {
"@solana/wallet-adapter-base": "^0.9.17",
"@solana/wallet-adapter-phantom": "^0.9.17",
"@solana/wallet-adapter-react": "^0.15.19",
"@solana/wallet-adapter-wallets": "^0.19.0",
"@solana/web3.js": "^1.62.0",
"@thirdweb-dev/sdk": "workspace:*",
"ethers": ">=5.5.1",
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
},
"peerDependenciesMeta": {
"@solana/wallet-adapter-base": {
"optional": true
},
"@solana/wallet-adapter-react": {
"optional": true
},
"@solana/wallet-adapter-wallets": {
"optional": true
},
"@solana/wallet-adapter-phantom": {
"optional": true
},
"@solana/web3.js": {
"optional": true
}
}
}
@@ -1,135 +0,0 @@
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.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),
);
-7
View File
@@ -1,7 +0,0 @@
{
"main": "dist/thirdweb-dev-react-solana.cjs.js",
"module": "dist/thirdweb-dev-react-solana.esm.js",
"browser": {
"./dist/thirdweb-dev-react-solana.esm.js": "./dist/thirdweb-dev-react-solana.browser.esm.js"
}
}
-5
View File
@@ -1,5 +0,0 @@
// Providers
export * from "./providers/full";
// re-export everything from react-core/solana
export * from "@thirdweb-dev/react-core/solana";
@@ -1,63 +0,0 @@
import type { WalletAdapter } from "@solana/wallet-adapter-base";
import { PhantomWalletAdapter } from "@solana/wallet-adapter-phantom";
import {
ConnectionProvider,
useWallet,
WalletProvider,
} from "@solana/wallet-adapter-react";
import { ThirdwebSDKProvider } from "@thirdweb-dev/react-core/solana";
import { getUrlForNetwork, Network } from "@thirdweb-dev/sdk/solana";
import { PropsWithChildren } from "react";
interface ThirdwebProviderProps {
network: Network;
wallets?: WalletAdapter[];
autoConnect?: boolean;
}
const DEFAULT_WALLETS = [new PhantomWalletAdapter()];
/**
* Gives access to the ThirdwebSDK instance and other useful hooks to the rest of the app.
* Requires to be wrapped with a ConnectionProvider and a WalletProvider from @solana/wallet-adapter-react.
* @example
* ```tsx
* import { ThirdwebProvider } from "@thirdweb-dev/react/solana";
*
* const App = () => {
* return (
* <ThirdwebProvider network="devnet">
* <YourApp />
* </ThirdwebProvider>
* )};
* ```
* @beta
*/
export const ThirdwebProvider: React.FC<
PropsWithChildren<ThirdwebProviderProps>
> = ({ network, wallets = DEFAULT_WALLETS, autoConnect = true, children }) => {
const clusterUrl = getUrlForNetwork(network);
return (
<ConnectionProvider endpoint={clusterUrl}>
<WalletProvider wallets={wallets} autoConnect={autoConnect}>
<ThirdwebWrapperProvider network={network}>
{children}
</ThirdwebWrapperProvider>
</WalletProvider>
</ConnectionProvider>
);
};
/**
* @internal
*/
export const ThirdwebWrapperProvider: React.FC<
PropsWithChildren<{ network?: Network }>
> = ({ network, children }) => {
const wallet = useWallet();
return (
<ThirdwebSDKProvider network={network} wallet={wallet}>
{children}
</ThirdwebSDKProvider>
);
};
-32
View File
@@ -1,32 +0,0 @@
import { Amount, NFTMetadataOrUri } from "@thirdweb-dev/sdk";
/**
* The params for the {@link useMintNFT} hook mutation.
*
* @beta
*/
export type MintNFTParams = {
to?: string;
metadata: NFTMetadataOrUri;
amount?: Amount;
};
/**
* The params for the {@link useClaimNFT} hook mutation.
*
* @beta
*/
export type ClaimNFTParams = {
to?: string;
amount: Amount;
};
/**
* The params to pass to {@link useMintNFTSupply} hook mutation
* @beta
*/
export type MintNFTSupplyParams = {
to?: string;
nftAddress: string;
amount: Amount;
};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,13 +0,0 @@
{
"pubkey": "cndy3Z4yapfJBmL3ShUp5exZKqR3z33thTzeNMm2gRZ",
"account": {
"lamports": 1141440,
"data": [
"AgAAAJaNEOMXm8NltKK1o+8Y5yxpsvMKz6JnKyxGPWPo7v1v",
"base64"
],
"owner": "BPFLoaderUpgradeab1e11111111111111111111111",
"executable": true,
"rentEpoch": 257
}
}
@@ -1,13 +0,0 @@
{
"pubkey": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s",
"account": {
"lamports": 1141440,
"data": [
"AgAAAAXgFWyn1dTpyEIhv4pL1ba/XlRKM81TyyALI0k8tGgQ",
"base64"
],
"owner": "BPFLoaderUpgradeab1e11111111111111111111111",
"executable": true,
"rentEpoch": 187
}
}
-46
View File
@@ -1,46 +0,0 @@
const path = require("path");
const MOCK_STORAGE_ID = "thirdweb-sdk";
function localDeployPath(programName) {
return path.join(__dirname, "test/solana/data", `${programName}.so`);
}
module.exports = {
validator: {
accountsCluster: "https://api.metaplex.solana.com",
accounts: [
{
label: "Candy Machine Program",
accountId: "cndy3Z4yapfJBmL3ShUp5exZKqR3z33thTzeNMm2gRZ",
executable: true,
},
{
label: "Token Metadata Program",
accountId: "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s",
// marking executable as true will cause Amman to pull the executable data account as well automatically
executable: true,
},
{
label: "Token Program",
accountId: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
executable: true,
},
],
programs: [
{
label: "Counter Program",
programId: "89RsF5yJgRXhae6LKuCcMRgXkqxCJm3AeaYwcJN4XopA",
deployPath: localDeployPath("counter"),
},
{
label: "TWRegistry Program",
programId: "twregzGdRmyFeAKjPgbPMkRkzgFNy8BHrB4HzwjyH14",
deployPath: localDeployPath("tw_registry"),
},
],
},
storage: {
storageId: MOCK_STORAGE_ID,
clearOnStart: true,
},
};
+1 -1
View File
@@ -6,7 +6,7 @@ SHELL := /bin/bash
test-evm: FORCE
docker start hh-node || docker run --name hh-node -d -p 8545:8545 ethereumoptimism/hardhat
./scripts/waitForHardhatNode.sh
pnpm run test:evm:all
pnpm run test:all
test-sol: FORCE
./scripts/run_tests.sh
-69
View File
@@ -1,69 +0,0 @@
<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 Solana SDK</h1>
<p align="center">
<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>thirdweb's Solana SDK for Browser, Node and React Native</strong></p>
<br />
## Installation
Install the latest version of the SDK with either `npm` or `yarn`:
```shell
npm install @thirdweb-dev/sdk
```
```shell
yarn add @thirdweb-dev/sdk
```
## Quick Start
The first thing to do to get started with Solana using thirdweb is to deploy a program. You can do this via the [Dashboard](https://thirdweb.com/dashboard), or via the SDK with the following snippet:
```jsx
import { ThirdwebSDK } from "@thirdweb-dev/sdk/solana";
// First, we instantiate the SDK and connect to Solana devnet
const sdk = ThirdwebSDK.fromNetwork("devnet");
// Next, we pass in a keypair to the SDK (you can generate this or use your own)
// You can also generate one, using Kepair.generate() from @solana/web3.js
const keypair = Keypair.fromSecretKey(...)
sdk.wallet.connect(keypair);
// Finally, we can deploy a new NFT Collection program
const address = await sdk.deployer.createNftCollection({
name: "My Collection",
});
```
Once we have a deployed program, we can access it using the SDK to read and write data to the program:
```jsx
// Here, we pass in the address of our deployed program
const program = await sdk.getNFTCollection(address);
// And now we can read data off our program, like getting all the NFTs from our collection
const nfts = await program.getAll();
// Or we can write data/send transactions to our program, like minting a new NFT
const mintAddress = await program.mint({
name: "New NFT",
});
const nft = await program.get(mintAddress);
```
## Learn More
You can learn more about thirdweb and the Solana SDK with the following resources:
- [Solana Docs](https://portal.thirdweb.com/solana)
- [Thirdweb Twitter](https://twitter.com/thirdweb_)
- [Thirdweb Discord](https://discord.com/invite/thirdweb)
@@ -1,342 +0,0 @@
/**
* 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-sdk-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": {
// . . .
// }
/**
* 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-wrong-input-file-type": {
"logLevel": "warning"
}
// "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
// },
//
// . . .
}
}
}
+5 -55
View File
@@ -24,13 +24,6 @@
},
"default": "./evm/dist/thirdweb-dev-sdk-evm.cjs.js"
},
"./solana": {
"module": {
"browser": "./solana/dist/thirdweb-dev-sdk-solana.browser.esm.js",
"default": "./solana/dist/thirdweb-dev-sdk-solana.esm.js"
},
"default": "./solana/dist/thirdweb-dev-sdk-solana.cjs.js"
},
"./evm/zksync": {
"module": {
"browser": "./evm/zksync/dist/thirdweb-dev-sdk-evm-zksync.browser.esm.js",
@@ -45,19 +38,11 @@
},
"default": "./evm/functions/dist/thirdweb-dev-sdk-evm-functions.cjs.js"
},
"./solana/server": {
"module": {
"browser": "./solana/server/dist/thirdweb-dev-sdk-solana-server.browser.esm.js",
"default": "./solana/server/dist/thirdweb-dev-sdk-solana-server.esm.js"
},
"default": "./solana/server/dist/thirdweb-dev-sdk-solana-server.cjs.js"
},
"./package.json": "./package.json"
},
"files": [
"dist/",
"evm/",
"solana/",
"server/"
],
"preconstruct": {
@@ -65,9 +50,7 @@
"index.ts",
"evm/index.ts",
"evm/functions/index.ts",
"evm/zksync/index.ts",
"solana/index.ts",
"solana/server/index.ts"
"evm/zksync/index.ts"
],
"exports": {
"envConditions": [
@@ -81,36 +64,24 @@
"lint": "eslint src/",
"fix": "eslint src/ --fix",
"clean": "rm -rf dist/",
"generate-docs": "pnpm generate-docs:evm && pnpm generate-docs:solana && pnpm generate-snippets",
"generate-docs": "pnpm generate-docs:evm && pnpm 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.mjs && node ./scripts/generate-feature-snippets-evm.mjs",
"build": "tsc && preconstruct build",
"test:evm:all": "SWC_NODE_PROJECT=./tsconfig.test.json nyc --reporter lcovonly --report-dir ./coverage/evm mocha --config './test/evm/.mocharc.json' --timeout 90000 --parallel './test/evm/**/*.test.ts'",
"test:evm": "make test-evm",
"test:evm:single": "SWC_NODE_PROJECT=./tsconfig.test.json mocha --config './test/evm/.mocharc.json' --timeout 90000",
"node:solana:start": "DEBUG='amman:(info|error|debug)' amman start --forceClone",
"node:solana:stop": "amman stop",
"test:solana:all": "SWC_NODE_PROJECT=./tsconfig.test.json nyc --reporter lcovonly --report-dir ./coverage/solana mocha --config './test/solana/.mocharc.json' --timeout 30000 --parallel './test/solana/**/*.test.ts'",
"test:solana": "make test-sol",
"test:solana:single": "SWC_NODE_PROJECT=./tsconfig.test.json mocha --config './test/solana/.mocharc.json' --timeout 30000",
"test:all": "SWC_NODE_PROJECT=./tsconfig.test.json nyc --reporter lcovonly --report-dir ./coverage/evm mocha --config './test/evm/.mocharc.json' --timeout 90000 --parallel './test/evm/**/*.test.ts'",
"test": "make test-evm",
"test:single": "SWC_NODE_PROJECT=./tsconfig.test.json mocha --config './test/evm/.mocharc.json' --timeout 90000",
"push": "yalc push"
},
"devDependencies": {
"@aws-sdk/client-secrets-manager": "^3.378.0",
"@ethersproject/abstract-provider": "^5.7.0",
"@metaplex-foundation/amman": "^0.12.1",
"@metaplex-foundation/amman-client": "^0.2.4",
"@metaplex-foundation/js": "^0.19.2",
"@metaplex-foundation/mpl-token-metadata": "^2.12.0",
"@microsoft/api-documenter": "^7.22.30",
"@microsoft/api-extractor": "^7.36.3",
"@microsoft/tsdoc": "^0.14.1",
"@nomiclabs/hardhat-ethers": "^2.2.3",
"@preconstruct/cli": "2.7.0",
"@project-serum/anchor": "^0.25.0",
"@solana/spl-token": "^0.3.5",
"@solana/web3.js": "^1.62.0",
"@swc-node/register": "^1.6.6",
"@swc/core": "^1.3.71",
"@thirdweb-dev/tsconfig": "workspace:*",
@@ -144,11 +115,6 @@
},
"peerDependencies": {
"@aws-sdk/client-secrets-manager": "^3.215.0",
"@metaplex-foundation/js": "^0.17.6",
"@metaplex-foundation/mpl-token-metadata": "^2.3.3",
"@project-serum/anchor": "^0.25.0",
"@solana/spl-token": "^0.3.5",
"@solana/web3.js": "^1.62.0",
"ethers": "^5",
"ethers-aws-kms-signer": "^1.3.2",
"zksync-web3": "^0.14.3"
@@ -160,21 +126,6 @@
"ethers-aws-kms-signer": {
"optional": true
},
"@solana/spl-token": {
"optional": true
},
"@solana/web3.js": {
"optional": true
},
"@project-serum/anchor": {
"optional": true
},
"@metaplex-foundation/mpl-token-metadata": {
"optional": true
},
"@metaplex-foundation/js": {
"optional": true
},
"zksync-web3": {
"optional": true
}
@@ -188,7 +139,6 @@
"bn.js": "^5.2.1",
"bs58": "^5.0.0",
"buffer": "^6.0.3",
"cross-fetch": "^3.1.8",
"eventemitter3": "^5.0.1",
"fast-deep-equal": "^3.1.3",
"merkletreejs": "^0.2.24",
+1 -1
View File
@@ -79,7 +79,7 @@ export function generateSnippets(ecosystem) {
return examples;
}
const path = ecosystem === "solana" ? "solana-sdk" : "typescript";
const path = "typescript";
const baseDocUrl = `https://portal.thirdweb.com/${path}/sdk.`;
const extractReferenceLink = (m, kind, contractName) => {
-21
View File
@@ -1,21 +0,0 @@
#!/usr/bin/env bash
set -e
sh -c "$(curl -sSfL https://release.solana.com/stable/install)"
export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH"
solana --version
pnpm run node:solana:start &
attempts=0
while ! curl 127.0.0.1:8899 &> /dev/null
do
if [ $attempts -gt 30 ]; then
printf "%s" "timed out waiting for solana node"
exit -1
fi
printf "%c" "."
sleep 1
attempts=$((attempts+1))
done
pnpm run test:solana:all
pnpm run node:solana:stop
-7
View File
@@ -1,7 +0,0 @@
{
"main": "dist/thirdweb-dev-sdk-solana.cjs.js",
"module": "dist/thirdweb-dev-sdk-solana.esm.js",
"browser": {
"./dist/thirdweb-dev-sdk-solana.esm.js": "./dist/thirdweb-dev-sdk-solana.browser.esm.js"
}
}
-7
View File
@@ -1,7 +0,0 @@
{
"main": "dist/thirdweb-dev-sdk-solana-server.cjs.js",
"module": "dist/thirdweb-dev-sdk-solana-server.esm.js",
"browser": {
"./dist/thirdweb-dev-sdk-solana-server.esm.js": "./dist/thirdweb-dev-sdk-solana-server.browser.esm.js"
}
}
-1
View File
@@ -1,5 +1,4 @@
import { ChainId } from "../constants/chains/ChainId";
import fetch from "cross-fetch";
import { BigNumber, utils, providers } from "ethers";
import { Mumbai, Polygon } from "@thirdweb-dev/chains";
import { isBrowser } from "./utils";
+2 -4
View File
@@ -8,7 +8,6 @@ import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { Abi } from "../schema/contracts/custom";
import { Contract, utils, providers } from "ethers";
import { EtherscanResult, VerificationStatus } from "../types/verification";
import fetch from "cross-fetch";
import { ConstructorParamMap } from "../types/any-evm/deploy-data";
import { getChainProvider } from "../constants/urls";
import invariant from "tiny-invariant";
@@ -464,9 +463,8 @@ async function fetchDeployBytecodeFromPublishedContractMetadata(
getChainProvider("polygon", {}),
) as ContractPublisher;
const publishedMetadataUri = await contract.getPublishedUriFromCompilerUri(
compilerMetaUri,
);
const publishedMetadataUri =
await contract.getPublishedUriFromCompilerUri(compilerMetaUri);
if (publishedMetadataUri.length === 0) {
throw Error(
`Could not resolve published metadata URI from ${compilerMetaUri}`,
@@ -1,6 +1,5 @@
import ForwarderABI from "@thirdweb-dev/contracts-js/dist/abis/Forwarder.json";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import fetch from "cross-fetch";
import {
BaseContract,
BigNumber,
@@ -42,7 +42,6 @@ import {
import { signEIP2612Permit } from "../../common/permit";
import { signTypedDataInternal } from "../../common/sign";
import ForwarderABI from "@thirdweb-dev/contracts-js/dist/abis/Forwarder.json";
import fetch from "cross-fetch";
import { BytesLike } from "ethers";
import { CONTRACT_ADDRESSES } from "../../constants/addresses/CONTRACT_ADDRESSES";
import { getContractAddressByChainId } from "../../constants/addresses/getContractAddressByChainId";
@@ -3,7 +3,6 @@ import { ChainId } from "../constants/chains/ChainId";
import { ContractWrapper } from "../core/classes/contract-wrapper";
import { SignedPayload721WithQuantitySignature } from "../schema/contracts/common/signature";
import { PrebuiltEditionDrop, PrebuiltNFTDrop } from "../types/eips";
// import fetch from "cross-fetch";
import invariant from "tiny-invariant";
const PAPER_API_BASE = `https://paper.xyz/api` as const;
@@ -1,230 +0,0 @@
import { TransactionResult } from "../types/common";
import {
NFTDropConditionsInput,
NFTDropConditions,
NFTDropUpdatableConditionsInputSchema,
} from "../types/programs/nft-drop";
import { toCurrencyValue } from "../utils/token";
import {
Amount,
CandyMachineV2EndSettings,
DateTime,
Metaplex,
sol,
toBigNumber,
toDateTime,
token,
} from "@metaplex-foundation/js";
import { PublicKey } from "@solana/web3.js";
/**
* Set the claim conditions for your NFT Drop to control how people can claim NFTs
*
* @example
* ```jsx
* const program = await sdk.getProgram("{{program_address}}", "nft-drop");
*
* // Get the data of the claim condition on the drop
* const claimCondition = await program.claimConditions.get();
* // View the price of the drop
* console.log(claimCondition.price);
* // View the date when the drop will go live
* console.log(claimCondition.goLiveDate);
* ```
*
* @public
*/
export class ClaimConditions {
private dropMintAddress: PublicKey;
private metaplex: Metaplex;
constructor(dropAddress: string, metaplex: Metaplex) {
this.dropMintAddress = new PublicKey(dropAddress);
this.metaplex = metaplex;
}
/**
* Get the claim condition for this contract
* @returns - The claim condition data for this NFT Drop
*
* @example
* ```jsx
* // Get the data of the claim condition on the drop
* const claimCondition = await program.claimConditions.get();
* // View the price of the drop
* console.log(claimCondition.price);
* // View the date when the drop will go live
* console.log(claimCondition.goLiveDate);
* ```
*/
async get(): Promise<NFTDropConditions> {
const candyMachine = await this.getCandyMachine();
const totalAvailableSupply = candyMachine.itemsAvailable.toNumber();
const lazyMintedSupply = candyMachine.itemsLoaded.toNumber();
const goLiveDate = candyMachine.goLiveDate
? new Date(candyMachine.goLiveDate.toNumber() * 1000)
: null;
const isWithinGoLiveDate = goLiveDate ? goLiveDate <= new Date() : true;
const maxClaimable =
candyMachine.endSettings && candyMachine.endSettings.endSettingType === 1
? candyMachine.endSettings.number.toNumber()
: "unlimited";
const claimedSupply = candyMachine.itemsMinted.toNumber();
const isBelowMaxClaimable =
maxClaimable !== "unlimited"
? claimedSupply < maxClaimable
: claimedSupply < lazyMintedSupply;
// TODO add allowlist/hidden settings here
return {
price: toCurrencyValue(candyMachine.price),
currencyAddress: candyMachine.tokenMintAddress?.toBase58() || null,
primarySaleRecipient: candyMachine.walletAddress.toBase58(),
sellerFeeBasisPoints: candyMachine.sellerFeeBasisPoints,
startTime: goLiveDate,
totalAvailableSupply,
lazyMintedSupply,
claimedSupply,
maxClaimable: maxClaimable.toString(),
isReadyToClaim:
candyMachine.isFullyLoaded && isWithinGoLiveDate && isBelowMaxClaimable,
};
}
async assertCanClaimable(quantity: number) {
const conditions = await this.get();
if (!conditions.isReadyToClaim) {
if (conditions.lazyMintedSupply < conditions.totalAvailableSupply) {
throw new Error(
`NFT Drop is not fully loaded yet. Only ${conditions.lazyMintedSupply} out of ${conditions.totalAvailableSupply} NFTs have been lazy minted.`,
);
}
if (conditions.maxClaimable === "0") {
throw new Error(
`Max Claimable is 0. No NFTs can be claimed. Set your claim conditions to enable claiming.`,
);
}
const maxClaimable =
conditions.maxClaimable === "unlimited"
? null
: parseInt(conditions.maxClaimable);
if (
(maxClaimable !== null &&
conditions.claimedSupply + quantity > maxClaimable) ||
conditions.claimedSupply + quantity > conditions.lazyMintedSupply
) {
throw new Error(
`Max claimable reached - ${conditions.claimedSupply} out of ${
conditions.maxClaimable !== null
? conditions.maxClaimable
: conditions.lazyMintedSupply
} NFTs have been claimed.`,
);
}
if (conditions.startTime && conditions.startTime > new Date()) {
throw new Error(
`Drop is not to ready be claimed yet, start date is ${conditions.startTime}`,
);
}
}
}
/**
* Set the claim condition settings to configure how people can claim your NFT Drop
* @param metadata - The settings to use for the claim condition of this program
* @returns - The transaction result of setting the claim condition
*
* @example
* ```jsx
* // Specify the settings for your claim condition
* const claimCondition = {
* // The price of each NFT in this drop (in SOL or SPL tokens)
* price: 0.1,
* // The date for this drop to start
* goLiveDate: new Date(),
* // ...
* };
*
* const tx = await program.claimConditions.set(claimCondition);
* ```
*/
async set(metadata: NFTDropConditionsInput): Promise<TransactionResult> {
const parsed = await NFTDropUpdatableConditionsInputSchema.parseAsync(
metadata,
);
// recipients
let wallet: PublicKey | undefined = parsed.primarySaleRecipient
? new PublicKey(parsed.primarySaleRecipient)
: undefined;
const sellerFeeBasisPoints = parsed.sellerFeeBasisPoints;
// price
let price: Amount | undefined = parsed.price
? sol(Number(parsed.price))
: undefined;
let tokenMint: PublicKey | undefined = undefined;
if (parsed.currencyAddress) {
const fetchedToken = await this.metaplex
.tokens()
.findMintByAddress({ address: new PublicKey(parsed.currencyAddress) });
price = token(Number(parsed.price || 0), fetchedToken.decimals);
tokenMint = fetchedToken.address;
wallet = this.metaplex
.tokens()
.pdas()
.associatedTokenAccount({
mint: tokenMint,
owner: wallet ? wallet : this.metaplex.identity().publicKey,
});
}
// dates
const goLiveDate: DateTime | undefined = parsed.startTime
? toDateTime(parsed.startTime)
: undefined;
// max claimable
const endSettings: CandyMachineV2EndSettings | null | undefined =
parsed.maxClaimable
? parsed.maxClaimable === "unlimited"
? null
: {
endSettingType: 1,
number: toBigNumber(parsed.maxClaimable),
}
: undefined;
// TODO add allowlist/hidden settings here
const data = {
...(wallet && { wallet }),
...(tokenMint
? { tokenMint }
: // if passing currencyAddress explicitly as null we need to honor that!
parsed.currencyAddress === null
? { tokenMint: null }
: {}),
...(price && { price }),
...(goLiveDate && { goLiveDate }),
...(sellerFeeBasisPoints && { sellerFeeBasisPoints }),
...(endSettings !== undefined && { endSettings }),
};
const result = await this.metaplex.candyMachinesV2().update({
candyMachine: await this.getCandyMachine(),
...data,
});
return {
signature: result.response.signature,
};
}
private async getCandyMachine() {
return this.metaplex
.candyMachinesV2()
.findByAddress({ address: this.dropMintAddress });
}
}
-328
View File
@@ -1,328 +0,0 @@
import {
NFTCollectionMetadataInput,
NFTCollectionMetadataInputSchema,
TokenMetadataInput,
TokenMetadataInputSchema,
} from "../types/programs";
import {
NFTDropContractInput,
NFTDropInitialConditionsInputSchema,
} from "../types/programs/nft-drop";
import { enforceCreator } from "./helpers/creators-helper";
import { Registry } from "./registry";
import {
getSignerHistogram,
Metaplex,
sol,
toBigNumber,
token,
TransactionBuilder,
} from "@metaplex-foundation/js";
import {
createCreateMetadataAccountV3Instruction,
DataV2,
} from "@metaplex-foundation/mpl-token-metadata";
import { Keypair } from "@solana/web3.js";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
/**
* Deploy new programs
*
* @example
* ```jsx
* import { ThirdwebSDK } from "@thirdweb-dev/sdk/solana";
*
* // Instantiate the SDK and pass in a signer
* const sdk = ThirdwebSDK.fromNetwork("devnet");
* sdk.wallet.connect(signer);
*
* // Define the metadata for your program
* const metadata = {
* name: "NFT Contract",
* image: readFileSync("files/image.jpg"),
* };
*
* // And deploy a new program from the connected wallet
* const address = await sdk.deployer.createNftCollection(metadata);
* ```
*
* @public
*/
export class Deployer {
private metaplex: Metaplex;
private storage: ThirdwebStorage;
private regsitry: Registry;
constructor(
registry: Registry,
metaplex: Metaplex,
storage: ThirdwebStorage,
) {
this.metaplex = metaplex;
this.storage = storage;
this.regsitry = registry;
}
/**
* Create a new token program
* @param tokenMetadata - the metadata of the token program
* @returns - the address of the new token program
*
* @example
* ```jsx
* const metadata = {
* name: "My Token",
* symbol: "TKN",
* initialSupply: 100,
* };
*
* const address = await sdk.deployer.createToken(metadata);
* ```
*/
async createToken(tokenMetadata: TokenMetadataInput): Promise<string> {
const tokenMetadataParsed = await TokenMetadataInputSchema.parseAsync(
tokenMetadata,
);
const uri = await this.storage.upload(tokenMetadataParsed);
const mint = Keypair.generate();
const owner = this.metaplex.identity().publicKey;
const mintTx = await this.metaplex
.tokens()
.builders()
.createTokenWithMint({
decimals: tokenMetadataParsed.decimals,
initialSupply: token(
tokenMetadataParsed.initialSupply,
tokenMetadataParsed.decimals,
),
mint,
});
const name = tokenMetadataParsed.name;
const data: DataV2 = {
name,
symbol: tokenMetadataParsed.symbol || "",
sellerFeeBasisPoints: 0,
uri,
creators: enforceCreator([], this.metaplex.identity().publicKey),
collection: null,
uses: null,
};
const metadata = this.metaplex
.nfts()
.pdas()
.metadata({ mint: mint.publicKey });
const metaTx = createCreateMetadataAccountV3Instruction(
{
metadata,
mint: mint.publicKey,
mintAuthority: owner,
payer: owner,
updateAuthority: owner,
},
{
createMetadataAccountArgsV3: {
isMutable: true,
data,
collectionDetails: null,
},
},
);
const registryInstructions =
await this.regsitry.getAddToRegistryInstructions(
mint.publicKey,
name,
"token",
);
await mintTx
.add({ instruction: metaTx, signers: [this.metaplex.identity()] })
.append(...registryInstructions)
.sendAndConfirm(this.metaplex);
return mint.publicKey.toBase58();
}
/**
* Create a new NFT collection program
* @param collectionMetadata - the metadata of the nft collection program
* @returns - the address of the new nft collection program
*
* @example
* ```jsx
* const metadata = {
* name: "My NFT Collection",
* symbol: "NFT",
* };
*
* const address = await sdk.deployer.createNftCollection(metadata);
* ```
*/
async createNftCollection(
collectionMetadata: NFTCollectionMetadataInput,
): Promise<string> {
const parsed = await NFTCollectionMetadataInputSchema.parseAsync(
collectionMetadata,
);
const uri = await this.storage.upload(parsed);
const collectionMint = Keypair.generate();
const name = parsed.name;
const collectionTx = await this.metaplex
.nfts()
.builders()
.create({
useNewMint: collectionMint,
name,
symbol: parsed.symbol,
sellerFeeBasisPoints: 0,
uri,
isCollection: true,
creators: enforceCreator(
parsed.creators,
this.metaplex.identity().publicKey,
),
});
const registryInstructions =
await this.regsitry.getAddToRegistryInstructions(
collectionMint.publicKey,
name,
"nft-collection",
);
const result = await collectionTx
.append(...registryInstructions)
.sendAndConfirm(this.metaplex);
if (!result.response.signature) {
throw new Error("Transaction failed");
}
return collectionMint.publicKey.toBase58();
}
/**
* Create a new NFT drop program
* @param metadata - the metadata of the nft drop program
* @returns - the address of the new nft drop program
*
* @example
* ```jsx
* const metadata = {
* name: "My NFT Drop",
* symbol: "NFT",
* totalSupply: 5,
* };
*
* const address = await sdk.deployer.createNftDrop(metadata);
* ```
*/
async createNftDrop(metadata: NFTDropContractInput): Promise<string> {
const collectionInfo = await NFTCollectionMetadataInputSchema.parseAsync(
metadata,
);
const candyMachineInfo =
await NFTDropInitialConditionsInputSchema.parseAsync(metadata);
const uri = await this.storage.upload(collectionInfo);
const collectionMint = Keypair.generate();
const name = collectionInfo.name;
const collectionTx = await this.metaplex
.nfts()
.builders()
.create({
useNewMint: collectionMint,
name,
symbol: collectionInfo.symbol,
sellerFeeBasisPoints: 0,
uri,
isCollection: true,
creators: enforceCreator(
collectionInfo.creators,
this.metaplex.identity().publicKey,
),
});
const candyMachineKeypair = Keypair.generate();
// initialize candy machine with default config
// final claim conditions can be updated later
const candyMachineTx = await this.metaplex
.candyMachinesV2()
.builders()
.create({
itemsAvailable: toBigNumber(candyMachineInfo.totalSupply),
price: sol(0),
sellerFeeBasisPoints: 0,
endSettings: {
endSettingType: 1,
number: toBigNumber(0),
},
candyMachine: candyMachineKeypair,
collection: collectionMint.publicKey,
creators: enforceCreator(
collectionInfo.creators,
this.metaplex.identity().publicKey,
),
});
const registryInstructions =
await this.regsitry.getAddToRegistryInstructions(
candyMachineKeypair.publicKey,
name,
"nft-drop",
);
// Have to split transactions here because it goes over the single transaction size limit
// We use `signAllTransactions` to sign all the transactions at once so the user only has to sign once
const block = await this.metaplex.connection.getLatestBlockhash();
const dropTx = collectionTx
.add(candyMachineTx)
.setFeePayer(this.metaplex.identity());
const regTransaction = TransactionBuilder.make()
.add(...registryInstructions)
.setFeePayer(this.metaplex.identity())
.toTransaction({
blockhash: block.blockhash,
lastValidBlockHeight: block.lastValidBlockHeight,
});
const dropSigners = [this.metaplex.identity(), ...dropTx.getSigners()];
const { keypairs } = getSignerHistogram(dropSigners);
const dropTransaction = dropTx.toTransaction({
blockhash: block.blockhash,
lastValidBlockHeight: block.lastValidBlockHeight,
});
// partially sign with the PDA keypairs
if (keypairs.length > 0) {
dropTransaction.partialSign(...keypairs);
}
// make the connected wallet sign both candyMachine + registry transactions
const signedTx = await this.metaplex
.identity()
.signAllTransactions([dropTransaction, regTransaction]);
// send the signed transactions *sequentially* the drop creation needs to succeed first before adding to registry
const signatures: string[] = [];
for (const tx of signedTx) {
signatures.push(
await this.metaplex.connection.sendRawTransaction(tx.serialize()),
);
}
// wait for confirmations in parallel
const confirmations = await Promise.all(
signatures.map((sig) => {
return this.metaplex.rpc().confirmTransaction(sig, {
blockhash: block.blockhash,
lastValidBlockHeight: block.lastValidBlockHeight,
});
}),
);
if (confirmations.length === 0) {
throw new Error("Transaction failed");
}
return candyMachineKeypair.publicKey.toBase58();
}
}
@@ -1,35 +0,0 @@
import { CreatorInput, CreatorOutput } from "../../types/programs";
import type { Creator } from "@metaplex-foundation/js";
import { PublicKey } from "@solana/web3.js";
/**
* @internal
*/
export function enforceCreator(
creators: CreatorInput[] = [],
owner: PublicKey,
): Creator[] {
if (creators.length === 0) {
// If no creators are specified, we assume the owner is the creator
creators = creators.concat({
address: owner.toBase58(),
share: 100,
verified: true,
});
}
return creators.map((creator) => ({
verified: creator.verified || false,
address: new PublicKey(creator.address),
share: creator.share,
}));
}
/**
* @internal
*/
export function parseCreators(creators: Creator[]): CreatorOutput[] {
return creators.map((creator) => ({
...creator,
address: creator.address.toBase58(),
}));
}
@@ -1,334 +0,0 @@
import { TokenStandard } from "@metaplex-foundation/mpl-token-metadata";
import {
QueryAllParams,
QueryAllParamsSchema,
} from "../../../core/schema/QueryParams";
import { NFT } from "../../../core/schema/nft";
import {
CANDYMACHINE_PROGRAM_ID,
METAPLEX_PROGRAM_ID,
} from "../../constants/addresses";
import { TransactionResult } from "../../types/common";
import { CreatorOutput } from "../../types/programs";
import { parseCreators } from "./creators-helper";
import {
findMasterEditionV2Pda,
GmaBuilder,
JsonMetadata,
Metadata,
Metaplex,
Mint,
Nft,
NftWithToken,
Sft,
SftWithToken,
token,
toMetadata,
toMetadataAccount,
toNftOriginalEdition,
toOriginalEditionAccount,
} from "@metaplex-foundation/js";
import { getAccount, getAssociatedTokenAddress } from "@solana/spl-token";
import {
ConfirmedSignatureInfo,
Connection,
ParsedAccountData,
PublicKey,
SignaturesForAddressOptions,
TransactionResponse,
} from "@solana/web3.js";
import BN from "bn.js";
/**
* @internal
*/
export class NFTHelper {
private metaplex: Metaplex;
private connection: Connection;
constructor(metaplex: Metaplex) {
this.metaplex = metaplex;
this.connection = metaplex.connection;
}
async getRaw(nftAddress: string) {
return await this.metaplex.nfts().findByMint({
mintAddress: new PublicKey(nftAddress),
});
}
async get(nftAddress: string): Promise<NFT> {
const meta = await this.getRaw(nftAddress);
return await this.toNFTMetadata(meta);
}
async transfer(
receiverAddress: string,
nftAddress: string,
quantity: number = 1,
): Promise<TransactionResult> {
const result = await this.metaplex.nfts().transfer({
nftOrSft: {
address: new PublicKey(nftAddress),
tokenStandard: TokenStandard.NonFungible,
},
toOwner: new PublicKey(receiverAddress),
amount: token(quantity, 0),
});
return {
signature: result.response.signature,
};
}
async creatorsOf(nftAddress: string): Promise<CreatorOutput[]> {
const meta = await this.getRaw(nftAddress);
return parseCreators(meta.creators);
}
async balanceOf(walletAddress: string, nftAddress: string): Promise<number> {
const address = await getAssociatedTokenAddress(
new PublicKey(nftAddress),
new PublicKey(walletAddress),
);
try {
const account = await getAccount(this.connection, address);
return Number(account.amount);
} catch (e) {
return 0;
}
}
async ownerOf(nftAddress: string): Promise<string | undefined> {
try {
const largestAccounts = await this.connection.getTokenLargestAccounts(
new PublicKey(nftAddress),
);
const largestAccountInfo = await this.connection.getParsedAccountInfo(
largestAccounts.value[0].address,
);
const parsedData = largestAccountInfo?.value?.data as ParsedAccountData;
const owner = parsedData ? parsedData.parsed.info.owner : undefined;
return owner;
} catch (err) {
return undefined;
}
}
async supplyOf(nftAddress: string): Promise<string> {
let originalEdition;
const originalEditionAccount = await this.metaplex
.rpc()
.getAccount(findMasterEditionV2Pda(new PublicKey(nftAddress)));
if (originalEditionAccount.exists) {
originalEdition = toNftOriginalEdition(
toOriginalEditionAccount(originalEditionAccount),
);
} else {
return "0";
}
// Add one to supply to account for the master edition
return originalEdition.supply.add(new BN(1)).toString();
}
async totalSupply(collectionAddress: string): Promise<number> {
const metadataAddresses = await this.getAllMetadataAddresses(
collectionAddress,
);
return metadataAddresses.length;
}
async getAll(
collectionAddress: string,
queryParams?: QueryAllParams,
): Promise<NFT[]> {
const { start, count } = QueryAllParamsSchema.parse(queryParams);
const metadataAddresses = await this.getAllMetadataAddresses(
collectionAddress,
);
// Metaplex GmaBuilder has a weird thing where they always start at 1 not 0
// so we workaround it by adding an extra address, and shifting the count to get the actual count we want
const fixedMetadataAddresses = (
start === 0 ? [PublicKey.default] : []
).concat(metadataAddresses);
const metadataInfos = await GmaBuilder.make(
this.metaplex,
fixedMetadataAddresses,
).getBetween(start, start === 0 ? count + 1 : start + count);
// Parse each account into a metadata account
const metadataParsed: Metadata<JsonMetadata<string>>[] = [];
for (const metadataInfo of metadataInfos) {
if (metadataInfo.exists) {
try {
metadataParsed.push(toMetadata(toMetadataAccount(metadataInfo)));
} catch (error) {
// no-op
}
}
}
// Finally, fetch the metadata + mint for each in parallel
const nfts = await Promise.all(
metadataParsed.map((m) => this.toNFTMetadata(m)),
);
return nfts;
}
async getTransactions(
collectionAddress: string,
options?: SignaturesForAddressOptions,
): Promise<TransactionResponse[]> {
const collectionKey = new PublicKey(collectionAddress);
// TODO cache signatures <> transactions mapping in memory so pagination doesn't re-request this everytime
const allSignatures: ConfirmedSignatureInfo[] = [];
if (options) {
// only fetch the specified options
allSignatures.push(
...(await this.metaplex.connection.getSignaturesForAddress(
collectionKey,
options,
)),
);
} else {
// fetch everything
// This returns the first 1000, so we need to loop through until we run out of signatures to get.
let signatures = await this.metaplex.connection.getSignaturesForAddress(
collectionKey,
);
allSignatures.push(...signatures);
do {
signatures = await this.metaplex.connection.getSignaturesForAddress(
collectionKey,
{
before: signatures[signatures.length - 1]?.signature,
},
);
allSignatures.push(...signatures);
} while (signatures.length > 0);
}
let txns: TransactionResponse[] = [];
// TODO RPC's will throttle this, need to do some optimizations here
const batchSize = 1000; // alchemy RPC batch limit
for (let i = 0; i < allSignatures.length; i += batchSize) {
const batch = allSignatures.slice(
i,
Math.min(allSignatures.length, i + batchSize),
);
txns = [
...txns,
...((
await this.metaplex.connection.getTransactions(
batch.map((s) => s.signature),
)
).filter((tx) => !!tx) as TransactionResponse[]),
];
}
return txns;
}
async getAllMetadataAddresses(
collectionAddress: string,
): Promise<PublicKey[]> {
const txns = (await this.getTransactions(collectionAddress)).reverse();
return txns
.map((tx) => {
if (tx) {
const programIds = tx.transaction.message
.programIds()
.map((p) => p.toString());
const accountKeys = tx.transaction.message.accountKeys.map((p) =>
p.toString(),
);
// Only look in transactions that call the Metaplex token metadata program
if (programIds.includes(METAPLEX_PROGRAM_ID)) {
// Go through all instructions in a given transaction
for (const ix of tx.transaction.message.instructions) {
// Filter for setAndVerify or verify instructions in the Metaplex token metadata program
if (
(ix.data === "K" || ix.data === "S" || ix.data === "X") &&
accountKeys[ix.programIdIndex] === METAPLEX_PROGRAM_ID
) {
const metadataAddressIndex = ix.accounts[0];
return tx.transaction.message.accountKeys[metadataAddressIndex];
}
}
} else if (programIds.includes(CANDYMACHINE_PROGRAM_ID)) {
for (const ix of tx.transaction.message.instructions) {
// filter for SetCollectionDuringMint from CandyMachineV2
if (
accountKeys[ix.programIdIndex] === CANDYMACHINE_PROGRAM_ID &&
ix.data === "JEuNFGs7wrU"
) {
const metadataAddressIndex = ix.accounts[1];
return tx.transaction.message.accountKeys[metadataAddressIndex];
}
}
}
}
})
.filter((a) => !!a) as PublicKey[];
}
async toNFTMetadata(
meta:
| Nft
| Sft
| NftWithToken
| SftWithToken
| Metadata<JsonMetadata<string>>,
): Promise<NFT> {
let mint = "mint" in meta ? meta.mint : undefined;
let fullModel = meta;
if (meta.model === "metadata") {
fullModel = await this.getRaw(meta.mintAddress.toBase58());
mint = fullModel.mint;
}
if (!mint) {
throw new Error("No mint found for NFT");
}
const [owner, supply] = await Promise.all([
this.ownerOf(mint.address.toBase58()),
this.supplyOf(mint.address.toBase58()),
]);
return this.toNFTMetadataResolved(mint, owner, supply, fullModel);
}
private async toNFTMetadataResolved(
mint: Mint,
owner: string | undefined,
supply: string,
fullModel:
| Nft
| Sft
| NftWithToken
| SftWithToken
| Metadata<JsonMetadata<string>>,
): Promise<NFT> {
return {
metadata: {
id: mint.address.toBase58(),
uri: fullModel.uri,
name: fullModel.name,
symbol: fullModel.symbol,
...fullModel.json,
},
owner: owner || PublicKey.default.toBase58(),
supply,
type: "metaplex",
};
}
}
-276
View File
@@ -1,276 +0,0 @@
import { TWREGISTRY_PROGRAM_ID } from "../constants/addresses";
import TWRegistryIDL from "../idl/tw_registry.json";
import { Program } from "../programs/program";
import { ProgramType } from "../programs/types";
import { WalletAccount } from "../types/common";
import { RegisteredProgram } from "../types/programs";
import { UserWallet } from "./user-wallet";
import {
CandyMachineV2,
InstructionWithSigners,
Metadata,
Metaplex,
Pda,
TokenGpaBuilder,
} from "@metaplex-foundation/js";
import { TokenStandard } from "@metaplex-foundation/mpl-token-metadata";
import type { BN, Idl } from "@project-serum/anchor";
import { PublicKey, Transaction } from "@solana/web3.js";
import { Buffer } from "buffer/";
/**
* @internal
*/
export class Registry {
private metaplex: Metaplex;
private twRegistry: Program;
constructor(metaplex: Metaplex, userWallet: UserWallet) {
this.metaplex = metaplex;
this.twRegistry = new Program(
TWREGISTRY_PROGRAM_ID,
TWRegistryIDL as Idl,
this.metaplex.connection,
userWallet,
);
}
public async getProgramType(address: string) {
try {
const candyMachine = await this.metaplex
.candyMachinesV2()
.findByAddress({ address: new PublicKey(address) });
if (candyMachine) {
return "nft-drop";
}
} catch (err) {
// ignore and try next
}
const metadata = await this.metaplex
.nfts()
.findByMint({ mintAddress: new PublicKey(address) });
if (metadata) {
if (metadata.collectionDetails) {
return "nft-collection";
} else {
if (metadata.tokenStandard === TokenStandard.Fungible) {
return "token";
}
}
}
throw new Error("Unknown account type");
}
public async getDeployedPrograms(
walletAddress: string,
): Promise<RegisteredProgram[]> {
const wallet = new PublicKey(walletAddress);
const count = await this.getTotalProgramsRegistered(wallet);
const programAddresses = [];
for (let i = 0; i < count; i++) {
programAddresses.push(
this.getRegisteredProgramAddress(wallet, i).toBase58(),
);
}
const programsRaw = await this.twRegistry.fetchMultiple(
"registeredProgramAccount",
programAddresses,
);
return programsRaw.map((raw) => this.toRegisteredProgram(raw));
}
public async getTotalProgramsRegistered(wallet: PublicKey) {
const accountExists = await this.registrarAccountExists(wallet);
if (!accountExists) {
return 0;
}
const registrarPda = this.getRegistrarAddress(wallet);
const data = await this.twRegistry.fetch(
"registrarAccount",
registrarPda.toBase58(),
);
return (data.count as BN).toNumber();
}
public async getProgramAt(
walletAddress: string,
index: number,
): Promise<RegisteredProgram> {
const wallet = new PublicKey(walletAddress);
const pda = this.getRegisteredProgramAddress(wallet, index);
const data = await this.twRegistry.fetch(
"registeredProgramAccount",
pda.toBase58(),
);
return this.toRegisteredProgram(data);
}
public getRegistrarAddress(wallet: PublicKey) {
return Pda.find(new PublicKey(TWREGISTRY_PROGRAM_ID), [
Buffer.from("registrar", "utf8"),
wallet.toBuffer(),
]);
}
public getRegisteredProgramAddress(wallet: PublicKey, index: number) {
return Pda.find(new PublicKey(TWREGISTRY_PROGRAM_ID), [
Buffer.from("registered_program"),
wallet.toBuffer(),
Buffer.from(index.toString()),
]);
}
/**
* @internal
*/
public async getAddToRegistryInstructions(
programToAdd: PublicKey,
programName: string,
programType: ProgramType,
) {
const wallet = this.metaplex.identity().publicKey;
const instructions: InstructionWithSigners[] = [];
const registrarAccountExists = await this.registrarAccountExists(wallet);
if (!registrarAccountExists) {
instructions.push({
instruction: (await this.getInitializeRegistrarTransaction(wallet))
.instructions[0],
signers: [this.metaplex.identity()],
});
}
instructions.push({
instruction: (
await this.getRegisterProgramTransaction(
wallet,
programToAdd,
programName,
programType,
)
).instructions[0],
signers: [this.metaplex.identity()],
});
return instructions;
}
private async getInitializeRegistrarTransaction(
wallet: PublicKey,
): Promise<Transaction> {
const registrarPda = this.getRegistrarAddress(wallet);
return this.twRegistry
.prepareCall("initializeRegistrar", {
accounts: {
authority: wallet.toBase58(),
registrarAccount: registrarPda.toBase58(),
},
})
.transaction();
}
private async getRegisterProgramTransaction(
wallet: PublicKey,
programAddress: PublicKey,
programName: string,
programType: string,
): Promise<Transaction> {
const registrarPda = this.getRegistrarAddress(wallet);
const registeredProgramAddress = this.getRegisteredProgramAddress(
wallet,
await this.getTotalProgramsRegistered(wallet),
);
return this.twRegistry
.prepareCall("register", {
accounts: {
authority: wallet.toBase58(),
registrarAccount: registrarPda.toBase58(),
registeredProgramAccount: registeredProgramAddress.toBase58(),
},
data: [programAddress, programName, programType],
})
.transaction();
}
private async registrarAccountExists(wallet: PublicKey) {
const registrarPda = this.getRegistrarAddress(wallet);
const account = await this.metaplex.rpc().getAccount(registrarPda);
return account.exists;
}
private getDropForCollection(
candyMachines: CandyMachineV2[],
meta: Metadata,
) {
return candyMachines.find(
(candyMachine) =>
candyMachine.collectionMintAddress?.toBase58() ===
meta.mintAddress.toBase58(),
);
}
// TODO probably don't need this anymore, rely on registry instead
private async getOwnedTokenAccounts(
walletAddress: string,
): Promise<WalletAccount[]> {
const mints = await this.getOwnedTokenAddreses(walletAddress);
const metadatas = await this.metaplex.nfts().findAllByMintList({ mints });
const candyMachines = await this.metaplex.candyMachinesV2().findAllBy({
type: "authority",
publicKey: new PublicKey(walletAddress),
});
return metadatas
.map((mintMetadata) => {
const meta = mintMetadata as Metadata;
if (!meta) {
return undefined;
}
if (meta?.collectionDetails) {
// check if it's part of a candy machine
const drop = this.getDropForCollection(candyMachines, meta);
if (drop) {
return {
type: "nft-drop",
address: drop.address.toBase58(),
name: meta.name,
};
} else {
return {
type: "nft-collection",
address: meta.mintAddress.toBase58(),
name: meta.name,
};
}
} else {
if (meta.tokenStandard === TokenStandard.Fungible) {
return {
type: "token",
address: meta.mintAddress.toBase58(),
name: meta.name,
};
}
}
})
.filter((account) => account !== undefined) as WalletAccount[];
}
private async getOwnedTokenAddreses(walletAddress: string) {
return await new TokenGpaBuilder(this.metaplex)
.selectMint()
.whereOwner(new PublicKey(walletAddress))
.getDataAsPublicKeys();
}
private toRegisteredProgram(
data: Record<string, unknown>,
): RegisteredProgram {
return {
deployer: (data.authority as PublicKey).toBase58(),
programAddress: (data.programAddress as PublicKey).toBase58(),
programName: (data.programName as string).toString(),
programType: (data.programType as string).toString(),
visible: data.visible as boolean,
};
}
}
@@ -1,205 +0,0 @@
import { CurrencyValue } from "../../core/schema/token";
import { WalletSigner } from "../types/common";
import { toCurrencyValue } from "../utils/token";
import { NFTHelper } from "./helpers/nft-helper";
import {
amount,
guestIdentity,
isIdentitySigner,
isKeypairSigner,
keypairIdentity,
Metaplex,
Signer,
SOL,
walletAdapterIdentity,
} from "@metaplex-foundation/js";
import {
Keypair,
PublicKey,
SignaturesForAddressOptions,
TransactionResponse,
} from "@solana/web3.js";
import bs58 from "bs58";
import EventEmitter from "eventemitter3";
import invariant from "tiny-invariant";
import nacl from "tweetnacl";
/**
*
* {@link UserWallet} events that you can subscribe to using `sdk.wallet.events`.
*
* @public
*/
export interface UserWalletEvents {
/**
* Emitted when `sdk.wallet.connect()` is called.
*/
connected: [WalletSigner];
/**
* Emitted when `sdk.wallet.disconnect()` is called.
*/
disconnected: void;
}
/**
* Handle and view info about the wallet connected to the SDK.
*
* @example
* ```jsx
* // Connect a wallet to the SDK, pass in a keypair or browser wallet adapter
* sdk.wallet.connect(signer)
*
* // Then you can read data about the connected wallet
* const address = sdk.wallet.getAddress();
* ```
*
* @public
*/
export class UserWallet {
public signer: Signer | undefined;
public events = new EventEmitter<UserWalletEvents>();
private metaplex: Metaplex;
public get network() {
const url = new URL(this.metaplex.connection.rpcEndpoint);
// try this first to avoid hitting `custom` network for alchemy urls
if (url.hostname.includes("devnet")) {
return "devnet";
}
if (url.hostname.includes("mainnet")) {
return "mainnet-beta";
}
return this.metaplex.cluster;
}
constructor(metaplex: Metaplex) {
this.metaplex = metaplex;
}
/**
* Connect a signer to the SDK. Can pass in a keypair or browser wallet adapter
* @param wallet - The signer to connect to the SDK
*
* @example
* ```jsx
* const signer = Keypair.generate();
* sdk.wallet.connect(signer);
* ```
*/
public connect(wallet: WalletSigner) {
this.connectToMetaplex(wallet);
this.events.emit("connected", wallet);
}
/**
* Disconnect the connected wallet from the SDK
*
* @example
* ```jsx
* sdk.wallet.disconnect();
* ```
*/
public disconnect() {
// TODO implement our own read only identity plugin with our own error messages
this.metaplex.use(guestIdentity());
this.events.emit("disconnected");
}
/**
* Return whether a wallet is connected
*/
public isConnected() {
return this.metaplex.identity().publicKey !== PublicKey.default;
}
/**
* Get the address of the connected wallet
* @returns the address of the connected wallet
*
* @example
* ```jsx
* const address = sdk.wallet.getAddress()
* ```
*/
public getAddress() {
return this.isConnected()
? this.metaplex.identity().publicKey.toBase58()
: undefined;
}
/**
* Get the connected signer
* @returns the signer
*
* @example
* ```jsx
* const signer = sdk.wallet.getSigner()
* ```
*/
public getSigner() {
return this.metaplex.identity();
}
public async sign(message: string): Promise<string> {
const signer = this.getSigner();
const encodedMessage = new TextEncoder().encode(message);
const signedMessage = await signer.signMessage(encodedMessage);
const signature = bs58.encode(signedMessage);
return signature;
}
public verifySignature(
message: string,
signature: string,
publicKey: string,
): boolean {
return nacl.sign.detached.verify(
new TextEncoder().encode(message),
bs58.decode(signature),
bs58.decode(publicKey),
);
}
/**
* Get the native balance of the connected wallet
* @returns the native balance currency value
*
* @example
* ```jsx
* const balance = await sdk.wallet.getBalance();
* console.log(balance.displayValue);
* ```
*/
public async getBalance(): Promise<CurrencyValue> {
const value = await this.metaplex.connection.getBalance(
this.metaplex.identity().publicKey,
);
return toCurrencyValue(amount(value, SOL));
}
/**
* Get the all transactions for this program
* @beta
*/
async getTransactions(
options?: SignaturesForAddressOptions,
): Promise<TransactionResponse[]> {
const helper = new NFTHelper(this.metaplex);
return helper.getTransactions(
this.metaplex.identity().publicKey.toBase58(),
options,
);
}
private connectToMetaplex(signer: WalletSigner) {
invariant(signer, "Wallet is not connected");
const plugin = isKeypairSigner(signer)
? keypairIdentity(Keypair.fromSecretKey(signer.secretKey))
: isIdentitySigner(signer)
? walletAdapterIdentity(signer)
: undefined;
invariant(plugin, "Wallet is not compatible with Metaplex");
this.metaplex.use(plugin);
}
}
@@ -1,17 +0,0 @@
/**
* @internal
*/
export const METAPLEX_PROGRAM_ID =
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
/**
* @internal
*/
export const CANDYMACHINE_PROGRAM_ID =
"cndy3Z4yapfJBmL3ShUp5exZKqR3z33thTzeNMm2gRZ";
/**
* @internal
*/
export const TWREGISTRY_PROGRAM_ID =
"twregzGdRmyFeAKjPgbPMkRkzgFNy8BHrB4HzwjyH14";
@@ -1,185 +0,0 @@
{
"version": "0.1.0",
"name": "tw_registry",
"instructions": [
{
"name": "initializeRegistrar",
"accounts": [
{
"name": "registrarAccount",
"isMut": true,
"isSigner": false
},
{
"name": "authority",
"isMut": true,
"isSigner": true
},
{
"name": "systemProgram",
"isMut": false,
"isSigner": false
}
],
"args": []
},
{
"name": "register",
"accounts": [
{
"name": "registrarAccount",
"isMut": true,
"isSigner": false
},
{
"name": "registeredProgramAccount",
"isMut": true,
"isSigner": false
},
{
"name": "authority",
"isMut": true,
"isSigner": true
},
{
"name": "systemProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "programAddress",
"type": "publicKey"
},
{
"name": "programName",
"type": "string"
},
{
"name": "programType",
"type": "string"
}
]
},
{
"name": "setVisibility",
"accounts": [
{
"name": "registeredProgramAccount",
"isMut": true,
"isSigner": false
},
{
"name": "authority",
"isMut": false,
"isSigner": true
}
],
"args": [
{
"name": "programIndex",
"type": "u64"
},
{
"name": "visible",
"type": "bool"
}
]
}
],
"accounts": [
{
"name": "RegistrarAccount",
"type": {
"kind": "struct",
"fields": [
{
"name": "count",
"type": "u64"
},
{
"name": "authority",
"type": "publicKey"
},
{
"name": "bump",
"type": "u8"
}
]
}
},
{
"name": "RegisteredProgramAccount",
"type": {
"kind": "struct",
"fields": [
{
"name": "authority",
"type": "publicKey"
},
{
"name": "programAddress",
"type": "publicKey"
},
{
"name": "programName",
"type": "string"
},
{
"name": "programType",
"type": "string"
},
{
"name": "visible",
"type": "bool"
},
{
"name": "bump",
"type": "u8"
}
]
}
}
],
"events": [
{
"name": "RegistrarInitializedEvent",
"fields": [
{
"name": "authority",
"type": "publicKey",
"index": false
}
]
},
{
"name": "ProgramRegisteredEvent",
"fields": [
{
"name": "authority",
"type": "publicKey",
"index": false
},
{
"name": "programAddress",
"type": "publicKey",
"index": false
},
{
"name": "programName",
"type": "string",
"index": false
},
{
"name": "programType",
"type": "string",
"index": false
}
]
}
],
"metadata": {
"address": "twregzGdRmyFeAKjPgbPMkRkzgFNy8BHrB4HzwjyH14"
}
}
-24
View File
@@ -1,24 +0,0 @@
// classes
export * from "./classes/user-wallet";
export * from "./classes/deployer";
// contracts
export * from "./programs/nft-collection";
export * from "./programs/nft-drop";
export * from "./programs/program";
export * from "./programs/token";
// sdk
export * from "./sdk";
// types
export * from "./types";
export * from "./types/common";
export * from "./types/programs";
export * from "./types/programs/nft-drop";
// utils
export * from "./utils/urls";
//schema for dashboard use
export {
TokenMetadataInputSchema,
NFTCollectionMetadataInputSchema,
} from "./types/programs";
export { NFTDropContractInputSchema } from "./types/programs/nft-drop";
@@ -1,598 +0,0 @@
import { QueryAllParams } from "../../core/schema/QueryParams";
import { NFT, NFTMetadata, NFTMetadataOrUri } from "../../core/schema/nft";
import { Amount } from "../../core/schema/shared";
import {
enforceCreator,
parseCreators,
} from "../classes/helpers/creators-helper";
import { NFTHelper } from "../classes/helpers/nft-helper";
import { TransactionResult } from "../types/common";
import { CreatorInput, CreatorOutput } from "../types/programs";
import { sendMultipartTransaction } from "../utils/transactions";
import { getNework } from "../utils/urls";
import {
findMasterEditionV2Pda,
Metaplex,
toBigNumber,
toNftOriginalEdition,
toOriginalEditionAccount,
TransactionBuilder,
} from "@metaplex-foundation/js";
import {
PublicKey,
SignaturesForAddressOptions,
TransactionResponse,
} from "@solana/web3.js";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import invariant from "tiny-invariant";
/**
* A collection of associated NFTs
*
* @example
* ```jsx
* import { ThirdwebSDK } from "@thirdweb-dev/sdk/solana";
*
* const sdk = ThirdwebSDK.fromNetwork("devnet");
* sdk.wallet.connect(signer);
*
* // Get the interface for your NFT collection program
* const program = await sdk.getProgram("{{program_address}}", "nft-collection");
* ```
*
* @public
*/
export class NFTCollection {
private metaplex: Metaplex;
private storage: ThirdwebStorage;
private nft: NFTHelper;
public publicKey: PublicKey;
public accountType = "nft-collection" as const;
public get network() {
return getNework(this.metaplex);
}
constructor(
collectionAddress: string,
metaplex: Metaplex,
storage: ThirdwebStorage,
) {
this.storage = storage;
this.metaplex = metaplex;
this.nft = new NFTHelper(metaplex);
this.publicKey = new PublicKey(collectionAddress);
}
/**
* Get the metadata for this program.
* @returns program metadata
*
* @example
* ```jsx
* const metadata = await program.getMetadata();
* console.log(metadata);
* ```
*/
async getMetadata(): Promise<NFTMetadata> {
const metadata = await this.getCollection();
return (await this.nft.toNFTMetadata(metadata)).metadata;
}
/**
* Get the creators of this program.
* @returns program metadata
*
* @example
* ```jsx
* const creators = await program.getCreators();
* console.log(creators);
* ```
*/
async getCreators(): Promise<CreatorOutput[]> {
const metadata = await this.getCollection();
return parseCreators(metadata.creators);
}
/**
* Get the royalty basis points for this collection
* @returns royalty basis points
*
* @example
* ```jsx
* const royalty = await program.getRoyalty();
* console.log(royalty);
* ```
*/
async getRoyalty(): Promise<number> {
const metadata = await this.getCollection();
return metadata.sellerFeeBasisPoints;
}
/**
* Get the metadata for a specific NFT
* @param nftAddress - the mint address of the NFT to get
* @returns the metadata of the NFT
*
* @example
* ```jsx
* // Specify the mint address of the NFT to get the data of
* const nftAddress = "...";
* // And get the data for the NFT
* const nft = await program.get(nftAddress);
* console.log(nft.metadata.name);
* console.log(nft.owner);
* ```
*/
async get(nftAddress: string): Promise<NFT> {
return this.nft.get(nftAddress);
}
/**
* Get the metadata for all NFTs on this collection
* @remarks This method is paginated. Use the `start` and `count` properties of the queryParams object to control pagination. By default the first 100 NFTs are returned
* @returns metadata for all minted NFTs
*
* @example
* ```jsx
* // Get all the NFTs that have been minted on this contract
* const nfts = await program.getAll();
* console.log(nfts[0].metadata.name);
* console.log(nfts[0].owner);
* ```
*/
async getAll(queryParams?: QueryAllParams): Promise<NFT[]> {
return this.nft.getAll(this.publicKey.toBase58(), queryParams);
}
/**
* Get the all transactions for this program
* @beta
*/
async getTransactions(
options?: SignaturesForAddressOptions,
): Promise<TransactionResponse[]> {
return this.nft.getTransactions(this.publicKey.toBase58(), options);
}
/**
* Get the total number of nfts minted on this program
* @returns the total number of NFTs minted on this program
*
* ```jsx
* const totalSupply = await program.totalSupply();
* ```
*/
async totalSupply(): Promise<number> {
return this.nft.totalSupply(this.publicKey.toBase58());
}
/**
* Get the NFT balance of the connected wallet
* @returns the NFT balance
*
* @example
* ```jsx
* // The mint address of the NFT to check the balance of
* const nftAddress = "..."
* // Get the NFT balance of the currently connected wallet
* const balance = await program.balance(nftAddress);
* console.log(balance);
* ```
*/
async balance(nftAddress: string): Promise<number> {
const address = this.metaplex.identity().publicKey.toBase58();
return this.balanceOf(address, nftAddress);
}
/**
* Get the NFT balance of the specified wallet
* @param walletAddress - the wallet address to get the balance of
* @param nftAddress - the mint address of the NFT to get the balance of
* @returns the NFT balance
*
* @example
* ```jsx
* // Specify the address of the wallet to get the balance of
* const walletAddress = "..."
* // Specify the mint address of the NFT to get the balance of
* const nftAddress = "..."
* const balance = await program.balanceOf(walletAddress, nftAddress);
* ```
*/
async balanceOf(walletAddress: string, nftAddress: string): Promise<number> {
return this.nft.balanceOf(walletAddress, nftAddress);
}
/**
* Get the current owner of the given NFT
* @param nftAddress - the mint address of the NFT to get the owner of
* @returns the owner of the NFT
* @example
* ```jsx
* const nftAddress = "..."
* const owner = await program.ownerOf(nftAddress);
* console.log(owner);
* ```
*/
async ownerOf(nftAddress: string): Promise<string | undefined> {
return this.nft.ownerOf(nftAddress);
}
/**
* Get the supply of NFT editions minted from a specific NFT
* @param nftAddress - the mint address of the NFT to check the supply of
* @returns the supply of the specified NFT
*
* @example
* ```jsx
* const address = "...";
* const supply = await program.supplyOf(address);
* ```
*/
async supplyOf(nftAddress: string): Promise<string> {
return this.nft.supplyOf(nftAddress);
}
/**
* Transfer the specified NFTs to another wallet
* @param receiverAddress - The address to send the tokens to
* @param nftAddress - The mint address of the NFT to transfer
* @returns the transaction result of the transfer
*
* @example
* ```jsx
* // The wallet address to transfer the NFTs to
* const to = "...";
* // The mint address of the NFT to transfer
* const nftAddress = "...";
* const tx = await program.transfer(to, nftAddress);
* ```
*/
async transfer(
receiverAddress: string,
nftAddress: string,
): Promise<TransactionResult> {
return this.nft.transfer(receiverAddress, nftAddress);
}
/**
* Mint NFTs to the connected wallet
* @param metadata - the metadata of the NFT to mint
* @returns the mint address of the minted NFT
*
* @example
* ```jsx
* // Add the metadata of your NFT
* const metadata = {
* name: "NFT #1",
* description: "My first NFT!",
* image: readFileSync("files/image.jpg"),
* properties: [
* {
* name: "coolness",
* value: "very cool!"
* }
* ]
* }
*
* // Then mint the new NFT and get its address
* const address = await program.mint(metadata);
* console.log(address);
* ```
*/
async mint(metadata: NFTMetadataOrUri): Promise<string> {
const address = this.metaplex.identity().publicKey.toBase58();
return this.mintTo(address, metadata);
}
/**
* Mint an NFT to the specified wallet
* @param to - the address to mint the NFT to
* @param metadata - the metadata of the NFT to mint
* @returns the mint address of the minted NFT
*
* @example
* ```jsx
* // Specify who to mint the NFT to
* const to = "...";
*
* // Add the metadata of your NFT
* const metadata = {
* name: "NFT #1",
* description: "My first NFT!",
* image: readFileSync("files/image.jpg"),
* properties: [
* {
* name: "coolness",
* value: "very cool!"
* }
* ]
* }
*
* // Then mint the new NFT and get its address
* const address = await program.mintTo(to, metadata);
* console.log(address);
* ```
*/
async mintTo(to: string, metadata: NFTMetadataOrUri) {
// TODO add options param for initial/maximum supply
const uri =
typeof metadata === "string"
? metadata
: await this.storage.upload(metadata);
const name =
typeof metadata === "string" ? "" : metadata.name?.toString() || "";
const collection = await this.getCollection();
const { nft } = await this.metaplex.nfts().create({
name,
uri,
creators: collection.creators,
sellerFeeBasisPoints: collection.sellerFeeBasisPoints,
collection: this.publicKey,
collectionAuthority: this.metaplex.identity(),
tokenOwner: new PublicKey(to),
// Always sets max supply to unlimited so editions can be minted
maxSupply: null,
});
return nft.address.toBase58();
}
/**
* Mint additional supply of an NFT to the connected wallet
* @param nftAddress - the mint address to mint additional supply to
* @param amount - the amount of additional NFTs to mint
* @returns the mint address of the minted NFT
*
* @example
* ```jsx
* // The address of the already minted NFT
* const nftAddress = "..."
* // The amount of additional NFTs to mint
* const amount = 1;
* // Mint an additional NFT of the original NFT
* const addresses = await program.mintAdditionalSupply(nftAddress, amount);
* ```
*/
async mintAdditionalSupply(nftAddress: string, amount: Amount) {
const address = this.metaplex.identity().publicKey.toBase58();
return this.mintAdditionalSupplyTo(address, nftAddress, amount);
}
/**
* Mint additional supply of an NFT to the specified wallet
* @param to - the address to mint the NFT to
* @param nftAddress - the mint address to mint additional supply to
* @param amount - the amount of NFTs to mint
* @returns the mint address of the minted NFT
*
* @example
* ```jsx
* // Specify who to mint the additional NFT to
* const to = "..."
* // The address of the already minted NFT
* const nftAddress = "..."
* * // The amount of additional NFTs to mint
* const amount = 1;
* // Mint an additional NFT of the original NFT
* const addresses = await program.mintAdditionalSupplyTo(to, nftAddress, amount);
* ```
*/
async mintAdditionalSupplyTo(
to: string,
nftAddress: string,
amount: Amount,
): Promise<string[]> {
// need to coerce amount to number always (to make the Array(<number>) work)
amount = typeof amount === "string" ? parseInt(amount) : amount;
// ensure that whatever got passed actually is a number now
invariant(
isNaN(amount) === false,
"amount must be possible to convert to a number",
);
// Better to use metaplex functions directly then our supplyOf function for types/consistency
const originalEditionAccount = await this.metaplex
.rpc()
.getAccount(findMasterEditionV2Pda(new PublicKey(nftAddress)));
const originalEdition = toNftOriginalEdition(
toOriginalEditionAccount(originalEditionAccount),
);
const builders = await Promise.all(
[...Array(amount).keys()].map(async (_, i) => {
return await this.metaplex
.nfts()
.builders()
.printNewEdition({
originalSupply: toBigNumber(
originalEdition.supply.add(toBigNumber(i)),
),
originalMint: new PublicKey(nftAddress),
newOwner: new PublicKey(to),
});
}),
);
const mintAddresses = builders.map((builder) =>
builder.getContext().mintSigner.publicKey.toBase58(),
);
await sendMultipartTransaction(builders, this.metaplex);
return mintAddresses;
}
/**
* Burn an NFT
* @param nftAddress - the mint address of the NFT to burn
* @returns the transaction signature
*
* @example
* ```jsx
* // Specify the address of the NFT to burn
* const nftAddress = "..."
* // And send the actual burn transaction
* const tx = await program.burn(nftAddress);
* ```
*/
async burn(nftAddress: string): Promise<TransactionResult> {
return this.burnBatch([nftAddress]).then((txs) => txs[0]);
}
/**
* Burn multiple NFTs
* @param nftAddresses - the mint addresses of the NFT to burn
* @returns the transaction signature
*
* @example
* ```jsx
* // Specify the address of the NFT to burn
* const nftAddress1 = "..."
* const nftAddress2 = "..."
* // And send the actual burn transaction
* const tx = await program.burnBatch([nftAddress1, nftAddress2]);
* ```
*/
async burnBatch(nftAddresses: string[]): Promise<TransactionResult[]> {
const txs: TransactionBuilder[] = [];
for (const nftAddress of nftAddresses) {
txs.push(
this.metaplex
.nfts()
.builders()
.delete({
mintAddress: new PublicKey(nftAddress),
collection: this.publicKey,
}),
);
}
return await sendMultipartTransaction(txs, this.metaplex);
}
/**
* SETTINGS
*/
/**
* Update the creators of the collection
* @param creators - the creators to update
* @param updateAll - whether or not to retroactively update the creators of all past NFTs
*/
async updateCreators(
creators: CreatorInput[],
updateAll: boolean = false,
): Promise<TransactionResult[]> {
if (updateAll) {
const txs: TransactionBuilder[] = [];
txs.push(
this.metaplex
.nfts()
.builders()
.update({
nftOrSft: await this.getCollection(),
creators: enforceCreator(
creators,
this.metaplex.identity().publicKey,
),
}),
);
// TODO: Don't use a get all here, all we need is mint addresses :(
const allNfts = await this.nft.getAll(this.publicKey.toBase58());
await Promise.all(
allNfts.map(async (nft) => {
const metaplexNft = await this.metaplex
.nfts()
.findByMint({ mintAddress: new PublicKey(nft.metadata.id) });
txs.push(
this.metaplex
.nfts()
.builders()
.update({
nftOrSft: metaplexNft,
creators: enforceCreator(
creators,
this.metaplex.identity().publicKey,
),
}),
);
}),
);
return await sendMultipartTransaction(txs, this.metaplex);
} else {
const tx = await this.metaplex.nfts().update({
nftOrSft: await this.getCollection(),
creators: enforceCreator(creators, this.metaplex.identity().publicKey),
});
return [
{
signature: tx.response.signature,
},
];
}
}
/**
* Update the royalty basis points of the collection
* @param sellerFeeBasisPoints - the royalty basis points of the collection
* @param updateAll - whether or not to retroactively update the royalty basis points of all past NFTs
*/
async updateRoyalty(
sellerFeeBasisPoints: number,
updateAll: boolean = false,
) {
if (updateAll) {
const txs: TransactionBuilder[] = [];
txs.push(
this.metaplex
.nfts()
.builders()
.update({
nftOrSft: await this.getCollection(),
sellerFeeBasisPoints,
}),
);
// TODO: Don't use a get all here, all we need is mint addresses :(
const allNfts = await this.nft.getAll(this.publicKey.toBase58());
await Promise.all(
allNfts.map(async (nft) => {
const metaplexNft = await this.metaplex
.nfts()
.findByMint({ mintAddress: new PublicKey(nft.metadata.id) });
txs.push(
this.metaplex.nfts().builders().update({
nftOrSft: metaplexNft,
sellerFeeBasisPoints,
}),
);
}),
);
return await sendMultipartTransaction(txs, this.metaplex);
} else {
const tx = await this.metaplex.nfts().update({
nftOrSft: await this.getCollection(),
sellerFeeBasisPoints,
});
return [
{
signature: tx.response.signature,
},
];
}
}
private async getCollection() {
return await this.metaplex
.nfts()
.findByMint({ mintAddress: this.publicKey });
}
}
@@ -1,689 +0,0 @@
import {
QueryAllParams,
QueryAllParamsSchema,
} from "../../core/schema/QueryParams";
import {
CommonNFTInput,
NFT,
NFTMetadata,
NFTMetadataInput,
} from "../../core/schema/nft";
import { Amount } from "../../core/schema/shared";
import { ClaimConditions } from "../classes/claim-conditions";
import {
enforceCreator,
parseCreators,
} from "../classes/helpers/creators-helper";
import { NFTHelper } from "../classes/helpers/nft-helper";
import { TransactionResult } from "../types/common";
import { CreatorInput, CreatorOutput } from "../types/programs";
import { sendMultipartTransaction } from "../utils/transactions";
import { getNework } from "../utils/urls";
import {
CandyMachineV2Item,
findCandyMachineV2CreatorPda,
Metaplex,
toBigNumber,
TransactionBuilder,
} from "@metaplex-foundation/js";
import {
PublicKey,
SignaturesForAddressOptions,
TransactionResponse,
} from "@solana/web3.js";
import { ThirdwebStorage, UploadProgressEvent } from "@thirdweb-dev/storage";
import invariant from "tiny-invariant";
const LAZY_MINT_BATCH_SIZE = 5;
/**
* A collection of NFTs that can be lazy minted and claimed
*
* @example
* ```jsx
* import { ThirdwebSDK } from "@thirdweb-dev/sdk/solana";
*
* const sdk = ThirdwebSDK.fromNetwork("devnet");
* sdk.wallet.connect(signer);
*
* // Get the interface for your NFT Drop program
* const program = await sdk.getProgram("{{program_address}}", "nft-drop");
* ```
*
* @public
*/
export class NFTDrop {
private metaplex: Metaplex;
private storage: ThirdwebStorage;
private nft: NFTHelper;
public accountType = "nft-drop" as const;
public publicKey: PublicKey;
public get network() {
return getNework(this.metaplex);
}
/**
* Manage the claim conditions for this drop
*
* @example
* ```jsx
* // set your claim conditions
* await program.claimConditions.set({
* maxClaimable: 100,
* price: 0.5,
* startTime: new Date(),
* });
*
* // get your claim conditions
* const conditions = await program.claimConditions.get();
* console.log(conditions.maxClaimable);
* console.log(conditions.claimedSupply);
* ```
*/
public claimConditions: ClaimConditions;
constructor(
dropAddress: string,
metaplex: Metaplex,
storage: ThirdwebStorage,
) {
this.storage = storage;
this.metaplex = metaplex;
this.nft = new NFTHelper(metaplex);
this.publicKey = new PublicKey(dropAddress);
this.claimConditions = new ClaimConditions(dropAddress, metaplex);
}
/**
* Get the metadata for this program.
* @returns program metadata
*
* @example
* ```jsx
* const metadata = await program.getMetadata();
* console.log(metadata.name);
* ```
*/
async getMetadata(): Promise<NFTMetadata> {
const info = await this.getCandyMachine();
invariant(info.collectionMintAddress, "Collection mint address not found");
const metadata = await this.metaplex
.nfts()
.findByMint({ mintAddress: info.collectionMintAddress });
return (await this.nft.toNFTMetadata(metadata)).metadata;
}
/**
* Get the creators of this program.
* @returns program metadata
*
* @example
* ```jsx
* const creators = await program.getCreators();
* console.log(creators);
* ```
*/
async getCreators(): Promise<CreatorOutput[]> {
const info = await this.getCandyMachine();
return parseCreators(info.creators);
}
/**
* Get the royalty basis points for this collection
* @returns royalty basis points
*
* @example
* ```jsx
* const royalty = await program.getRoyalty();
* console.log(royalty);
* ```
*/
async getRoyalty(): Promise<number> {
const info = await this.getCandyMachine();
return info.sellerFeeBasisPoints;
}
/**
* Get the metadata for a specific NFT
* @param nftAddress - the mint address of the NFT to get
* @returns the metadata of the NFT
*
* @example
* ```jsx
* // Specify the mint address of the NFT to get the data of
* const nftAddress = "...";
* // And get the data for the NFT
* const nft = await program.get(nftAddress);
*
* console.log(nft.name);
* ```
*/
async get(nftAddress: string): Promise<NFT> {
return this.nft.get(nftAddress);
}
/**
* Get the metadata for all NFTs on this drop
* @returns metadata for all minted NFTs
*
* @example
* ```jsx
* // Get all the NFTs that have been minted on this contract
* const nfts = await program.getAll();
*
* console.log(nfts[0].metadata.name);
* ```
*/
async getAll(queryParams?: QueryAllParams): Promise<NFT[]> {
const parsedQueryParams = QueryAllParamsSchema.parse(queryParams);
const info = await this.getCandyMachine();
// First, get all the claimed NFTs within the query params range
const claimedNfts = await this.getAllClaimed(parsedQueryParams);
const totalClaimed = await this.totalClaimedSupply();
// Then filter out all claimed NFTs from items to leave only unclaimed remaining
const unclaimedItems: CandyMachineV2Item[] = [];
info.items.forEach((item) => {
const isClaimed =
claimedNfts.filter(
(nft) =>
nft.metadata.name === item.name && nft.metadata.uri === item.uri,
).length > 0;
if (!isClaimed) {
unclaimedItems.push(item);
}
});
// Only fill the remaining count left over after claimed NFTs with unclaimed NFTs
const startIndex = Math.max(0, parsedQueryParams.start - totalClaimed);
const endIndex = Math.max(
0,
startIndex + parsedQueryParams.count - claimedNfts.length,
);
const unclaimedNfts = await Promise.all(
unclaimedItems.slice(startIndex, endIndex).map(async (item) => {
const metadata: NFTMetadata = await this.storage.downloadJSON(item.uri);
return {
metadata: {
...metadata,
id: PublicKey.default.toBase58(),
uri: item.uri,
},
owner: PublicKey.default.toBase58(),
supply: "0",
type: "metaplex",
} as NFT;
}),
);
// Always return claimed NFTs first, and then fill remaining query count with unclaimed NFTs
return [...claimedNfts, ...unclaimedNfts];
}
/**
* Get the all transactions for this program
* @beta
*/
async getTransactions(
options?: SignaturesForAddressOptions,
): Promise<TransactionResponse[]> {
return this.nft.getTransactions(this.publicKey.toBase58(), options);
}
/**
* Get the metadata for all the claimed NFTs on this drop
* @returns metadata for all claimed NFTs
*
* @example
* ```jsx
* // Get all the NFTs that have already been claimed from this drop
* const nfts = await program.getAllClaimed();
* console.log(nfts[0].name)
* ```
*/
async getAllClaimed(queryParams?: QueryAllParams): Promise<NFT[]> {
// using getAll from collection here because candy machin findAllMinted doesn't return anything
const candy = await this.getCandyMachine();
invariant(candy.collectionMintAddress, "Collection mint address not found");
return await this.nft.getAll(
candy.collectionMintAddress.toBase58(),
queryParams,
);
}
/**
* Get the NFT balance of the connected wallet
* @returns the NFT balance
*
* @example
* ```jsx
* // The mint address of the NFT to check the balance of
* const nftAddress = "..."
* // Get the NFT balance of the currently connected wallet
* const balance = await program.balance(nftAddress);
* console.log(balance);
* ```
*/
async balance(nftAddress: string): Promise<number> {
const address = this.metaplex.identity().publicKey.toBase58();
return this.balanceOf(address, nftAddress);
}
/**
* Get the NFT balance of the specified wallet
* @param walletAddress - the wallet address to get the balance of
* @param nftAddress - the mint address of the NFT to get the balance of
* @returns the NFT balance
*
* @example
* ```jsx
* // The address of the wallet to check the balance of
* const walletAddress = "..."
* // The mint address of the NFT to check the balance of
* const nftAddress = "..."
* // Get the actual NFT balance of the specified wallet
* const balance = await program.balanceOf(walletAddress, nftAddress);
* ```
*/
async balanceOf(walletAddress: string, nftAddress: string): Promise<number> {
return this.nft.balanceOf(walletAddress, nftAddress);
}
/**
* Get the current owner of the given NFT
* @param nftAddress - the mint address of the NFT to get the owner of
* @returns the owner of the NFT
* @example
* ```jsx
* const nftAddress = "..."
* const owner = await program.ownerOf(nftAddress);
* console.log(owner);
* ```
*/
async ownerOf(nftAddress: string): Promise<string | undefined> {
return this.nft.ownerOf(nftAddress);
}
/**
* Get the total minted supply of this drop
* @returns the total supply
*
* @example
* ```jsx
* // Get the total number of NFTs that have been minted on this drop
* const supply = await program.totalSupply();
* ```
*/
async totalSupply(): Promise<number> {
const info = await this.getCandyMachine();
return info.itemsLoaded.toNumber();
}
/**
* Get the total unclaimed supply of this drop
* @returns the total supply
*
* @example
* ```jsx
* // Get the total number of lazy minted NFTs that aren't yet claimed
* const supply = await program.totalUnclaimedSupply();
* ```
*/
async totalUnclaimedSupply(): Promise<number> {
const info = await this.getCandyMachine();
return Math.min(
info.itemsLoaded.toNumber(),
info.itemsRemaining.toNumber(),
);
}
/**
* Get the total claimed supply of this drop
* @returns the total supply
*
* @example
* ```jsx
* // Get the total number of lazy minted NFTs that have already been claimed
* const supply = await program.totalClaimedSupply();
* ```
*/
async totalClaimedSupply(): Promise<number> {
const info = await this.getCandyMachine();
return info.itemsMinted.toNumber();
}
/**
* Transfer the specified NFTs to another wallet
* @param receiverAddress - The address to send the tokens to
* @param nftAddress - The mint address of the NFT to transfer
* @returns the transaction result of the transfer
*
* @example
* ```jsx
* // The wallet address to transfer the NFTs to
* const to = "...";
* // The mint address of the NFT to transfer
* const nftAddress = "...";
* const tx = await program.transfer(to, nftAddress);
* ```
*/
async transfer(
receiverAddress: string,
nftAddress: string,
): Promise<TransactionResult> {
return this.nft.transfer(receiverAddress, nftAddress);
}
/**
* Lazy mint NFTs to be claimed later
* @param metadatas - The metadata of the NFTs to lazy mint
* @param options
* @returns the transaction result of the lazy mint
*
* @example
* ```jsx
* // Add the metadata of your NFTs
* const metadata = [
* {
* name: "NFT #1",
* description: "My first NFT!",
* image: readFileSync("files/image.jpg"),
* properties: [
* {
* name: "coolness",
* value: "very cool!"
* }
* ]
* }
* ];
*
* // And lazy mint NFTs to your program
* const tx = await program.lazyMint(metadatas);
* ```
*/
async lazyMint(
metadatas: NFTMetadataInput[],
options?: {
onProgress: (event: UploadProgressEvent) => void;
},
): Promise<TransactionResult[]> {
const candyMachine = await this.getCandyMachine();
const parsedMetadatas = metadatas.map((metadata) =>
CommonNFTInput.parse(metadata),
);
const uris = await this.storage.uploadBatch(parsedMetadatas, options);
const items: CandyMachineV2Item[] = uris.map((uri, i) => ({
name: parsedMetadatas[i].name?.toString() || "",
uri,
}));
// turn items into batches of $LAZY_MINT_BATCH_SIZE
const batches: CandyMachineV2Item[][] = [];
while (items.length) {
batches.push(items.splice(0, LAZY_MINT_BATCH_SIZE));
}
const builders = batches.map((batch, i) =>
this.metaplex
.candyMachinesV2()
.builders()
.insertItems({
candyMachine,
authority: this.metaplex.identity(),
items: batch,
index: toBigNumber(
i * LAZY_MINT_BATCH_SIZE + candyMachine.itemsLoaded.toNumber(),
),
}),
);
return await sendMultipartTransaction(builders, this.metaplex);
}
/**
* Claim an NFT from the drop with connected wallet
* @returns - the mint address of the claimed NFT
*
* @example
* ```jsx
* // Specify the quantity of NFTs to claim
* const quantity = 1;
* // Claim NFTs and get their mint addresses
* const claimedAddresses = await program.claim(quantity);
* console.log("Claimed NFT at address", claimedAddresses[0]);
* ```
*/
async claim(amount: Amount): Promise<string[]> {
const address = this.metaplex.identity().publicKey.toBase58();
return this.claimTo(address, amount);
}
/**
* Claim an NFT from the drop for the specified wallet
* @returns - the mint address of the claimed NFT
*
* @example
* ```jsx
* // Specify which address to claim the NFTs to
* const receiverAddress = "...";
* // Claim the NFTs to the specified wallet and get the mint addresses of the NFTs
* const claimedAddresses = await program.claimTo(receiverAddress, 1);
* console.log("Claimed NFT at address", claimedAddresses[0]);
* ```
*/
async claimTo(receiverAddress: string, amount: Amount): Promise<string[]> {
const candyMachine = await this.getCandyMachine();
await this.claimConditions.assertCanClaimable(Number(amount));
const builders = await Promise.all(
[...Array(Number(amount)).keys()].map(async () => {
return await this.metaplex
.candyMachinesV2()
.builders()
.mint({
candyMachine,
newOwner: new PublicKey(receiverAddress),
});
}),
);
const mintAddresses = builders.map((builder) =>
builder.getContext().mintSigner.publicKey.toBase58(),
);
await sendMultipartTransaction(builders, this.metaplex);
return mintAddresses;
}
/**
* Burn an NFT
* @param nftAddress - the mint address of the NFT to burn
* @returns the transaction signature
*
* @example
* ```jsx
* // Specify the address of the NFT to burn
* const nftAddress = "..."
* // And send the actual burn transaction
* const tx = await program.burn(nftAddress);
* ```
*/
async burn(nftAddress: string): Promise<TransactionResult> {
return this.burnBatch([nftAddress]).then((txs) => txs[0]);
}
/**
* Burn multiple NFTs
* @param nftAddresses - the mint addresses of the NFT to burn
* @returns the transaction signature
*
* @example
* ```jsx
* // Specify the address of the NFT to burn
* const nftAddress1 = "..."
* const nftAddress2 = "..."
* // And send the actual burn transaction
* const tx = await program.burnBatch([nftAddress1, nftAddress2]);
* ```
*/
async burnBatch(nftAddresses: string[]): Promise<TransactionResult[]> {
const candyMachine = await this.getCandyMachine();
const collection = candyMachine.collectionMintAddress
? candyMachine.collectionMintAddress
: undefined;
const txs: TransactionBuilder[] = [];
for (const nftAddress of nftAddresses) {
txs.push(
this.metaplex
.nfts()
.builders()
.delete({
mintAddress: new PublicKey(nftAddress),
collection,
}),
);
}
return await sendMultipartTransaction(txs, this.metaplex);
}
/**
* Update the creators of the collection
* @param creators - the creators to update
* @param updateAll - whether or not to retroactively update the creators of all past NFTs
*/
async updateCreators(
creators: CreatorInput[],
updateAll: boolean = false,
): Promise<TransactionResult[]> {
if (updateAll) {
const txs: TransactionBuilder[] = [];
const candyMachine = await this.getCandyMachine();
const collectionAddress = candyMachine.collectionMintAddress;
invariant(collectionAddress, "Collection mint address not found");
// We need to find the candy machine creator PDA which is auto populated onto NFTs
// minted with the candy machine, and add it to the list of creators for each NFT
const candyMachineCreatorPda = findCandyMachineV2CreatorPda(
candyMachine.address,
);
const nftCreators = [
{
address: candyMachineCreatorPda.toBase58(),
share: 0,
},
...creators,
];
const allNfts = await this.nft.getAll(collectionAddress.toBase58());
await Promise.all(
allNfts.map(async (nft) => {
const metaplexNft = await this.metaplex
.nfts()
.findByMint({ mintAddress: new PublicKey(nft.metadata.id) });
txs.push(
this.metaplex
.nfts()
.builders()
.update({
nftOrSft: metaplexNft,
creators: enforceCreator(
nftCreators,
this.metaplex.identity().publicKey,
),
}),
);
}),
);
const results = await sendMultipartTransaction(txs, this.metaplex);
const tx = await this.metaplex.candyMachinesV2().update({
candyMachine: await this.getCandyMachine(),
creators: enforceCreator(creators, this.metaplex.identity().publicKey),
});
return [
{
signature: tx.response.signature,
},
...results,
];
} else {
const tx = await this.metaplex.candyMachinesV2().update({
candyMachine: await this.getCandyMachine(),
creators: enforceCreator(creators, this.metaplex.identity().publicKey),
});
return [
{
signature: tx.response.signature,
},
];
}
}
/**
* Update the royalty basis points of the collection
* @param sellerFeeBasisPoints - the royalty basis points of the collection
* @param updateAll - whether or not to retroactively update the royalty basis points of all past NFTs
*/
async updateRoyalty(
sellerFeeBasisPoints: number,
updateAll: boolean = false,
) {
if (updateAll) {
const txs: TransactionBuilder[] = [];
const candyMachine = await this.getCandyMachine();
const collectionAddress = candyMachine.collectionMintAddress;
invariant(collectionAddress, "Collection mint address not found");
const allNfts = await this.nft.getAll(collectionAddress.toBase58());
await Promise.all(
allNfts.map(async (nft) => {
const metaplexNft = await this.metaplex
.nfts()
.findByMint({ mintAddress: new PublicKey(nft.metadata.id) });
txs.push(
this.metaplex.nfts().builders().update({
nftOrSft: metaplexNft,
sellerFeeBasisPoints,
}),
);
}),
);
const results = await sendMultipartTransaction(txs, this.metaplex);
const tx = await this.metaplex.candyMachinesV2().update({
candyMachine: await this.getCandyMachine(),
sellerFeeBasisPoints,
});
return [
{
signature: tx.response.signature,
},
...results,
];
} else {
const tx = await this.metaplex.candyMachinesV2().update({
candyMachine: await this.getCandyMachine(),
sellerFeeBasisPoints,
});
return {
signature: tx.response.signature,
};
}
}
private async getCandyMachine() {
return this.metaplex
.candyMachinesV2()
.findByAddress({ address: this.publicKey });
}
}
-145
View File
@@ -1,145 +0,0 @@
import { UserWallet } from "../classes/user-wallet";
import { Cluster, resolveClusterFromConnection } from "@metaplex-foundation/js";
import {
Program as AnchorProgram,
Idl,
AnchorProvider,
setProvider,
} from "@project-serum/anchor";
import { Connection, PublicKey, Signer } from "@solana/web3.js";
/**
* Dynamic interface for interacting with Solana programs.
*
* @example
* ```jsx
* import { ThirdwebSDK } from "@thirdweb-dev/sdk/solana";
*
* const sdk = ThirdwebSDK.fromNetwork("devnet");
* sdk.wallet.connect(signer);
*
* // Get the interface for your program
* const program = await sdk.getProgram("{{program_address}}");
* ```
*
* @public
*/
export class Program {
private program: AnchorProgram<Idl>;
public publicKey: PublicKey;
public accountType = "program" as const;
public network: Cluster;
constructor(
programAddress: string,
idl: Idl,
connection: Connection,
wallet: UserWallet,
) {
this.publicKey = new PublicKey(programAddress);
this.network = resolveClusterFromConnection(connection);
setProvider(new AnchorProvider(connection, wallet.getSigner(), {}));
this.program = new AnchorProgram(idl, programAddress);
wallet.events.on("connected", () => {
setProvider(new AnchorProvider(connection, wallet.getSigner(), {}));
});
wallet.events.on("disconnected", () => {
// identity will be guest in this case
setProvider(new AnchorProvider(connection, wallet.getSigner(), {}));
});
}
/**
* Call a function on this program
* @param functionName - Name of the function to call
* @param args - Arguments to pass to the function including accounts, data, and signers
* @returns result of the contract call
*
* @example
* ```jsx
* const counterAccount = Keypair.generate();
* await program.call("increment", {
* // We need to pass in the public keys of any accounts to interact with
* accounts: {
* counterAccount: counterAccount.publicKey.toBase58(),
* },
* // As well as the arguments to pass to the data parameters
* data: ["..."],
* // And the signer of the account that will be signing the message
* signers: [counterAccount]
* })
* ```
*/
async call(
functionName: string,
args: {
accounts: Record<string, string>;
data?: any[];
signers?: Signer[];
},
) {
return await this.prepareCall(functionName, args).rpc();
}
prepareCall(
functionName: string,
args: {
accounts: Record<string, string>;
data?: any[];
signers?: Signer[];
},
) {
const fn = this.program.methods[functionName];
if (!fn) {
throw new Error(`Function ${functionName} not found`);
}
const fnWithArgs = args.data ? fn(...args.data) : fn();
return fnWithArgs.accounts(args.accounts).signers(args.signers || []);
}
/**
* Read account data associated with this program
* @param accountName - The name of the account type to fetch the data of
* @param address - The address of the specific account to fetch
* @returns - The data of the requested account
*
* @example
* ```jsx
* const accountAddress = "...";
* // Get the counterAccount at specified address
* const counterAccount = await program.fetch("counterAccount", accountaddress);
* ```
*/
async fetch(accountName: string, address: string) {
const account = this.program.account[accountName];
if (!account) {
throw new Error(`Account ${account} not found`);
}
return await account.fetch(address);
}
/**
* Read multiple accounts data associated with this program
* @param accountName - The name of the account type to fetch the data of
* @param addresses - The addresses of the each account to fetch
* @returns - The data of the requested accounts
*
* @example
* ```jsx
* const accountAddresses = ["...", "..."];
* const counterAccounts = await program.fetchMultiple("counterAccount", accountAddresses);
* ```
*/
async fetchMultiple(
accountName: string,
addresses: string[],
): Promise<Record<string, unknown>[]> {
const account = this.program.account[accountName];
if (!account) {
throw new Error(`Account ${account} not found`);
}
return (await account.fetchMultiple(addresses)).filter(
(a) => a !== null,
) as Record<string, unknown>[];
}
}
-220
View File
@@ -1,220 +0,0 @@
import { Amount, AmountSchema } from "../../core/schema/shared";
import { CurrencyValue, TokenMetadata } from "../../core/schema/token";
import { TransactionResult } from "../types/common";
import { toCurrencyValue } from "../utils/token";
import { getNework } from "../utils/urls";
import { Metaplex, toBigNumber, token } from "@metaplex-foundation/js";
import { getAccount, getAssociatedTokenAddress } from "@solana/spl-token";
import { Connection, PublicKey } from "@solana/web3.js";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
/**
* Standard token or cryptocurrency.
*
* @example
* ```jsx
* import { ThirdwebSDK } from "@thirdweb-dev/sdk/solana";
*
* const sdk = ThirdwebSDK.fromNetwork("devnet");
* sdk.wallet.connect(signer);
*
* // Get the interface for your token program
* const program = await sdk.getProgram("{{program_address}}", "token");
* ```
*
* @public
*/
export class Token {
private connection: Connection;
private metaplex: Metaplex;
private storage: ThirdwebStorage;
public accountType = "token" as const;
public publicKey: PublicKey;
public get network() {
return getNework(this.metaplex);
}
constructor(
tokenAddress: string,
metaplex: Metaplex,
storage: ThirdwebStorage,
) {
this.storage = storage;
this.metaplex = metaplex;
this.connection = metaplex.connection;
this.publicKey = new PublicKey(tokenAddress);
}
/**
* Get the metadata for this token including the name, supply, and decimals.
* @returns Token metadata
*
* @example
* ```jsx
* const metadata = await program.getMetadata();
* console.log(metadata.supply);
* console.log(metadata.decimals);
* ```
*/
async getMetadata(): Promise<TokenMetadata> {
const mint = await this.getMint();
const meta = await this.metaplex.nfts().findByMint({
mintAddress: mint.address,
});
return {
...meta.json,
id: meta.address.toBase58(),
uri: meta.uri,
name: meta.name,
symbol: meta.symbol,
decimals: mint.decimals,
supply: toCurrencyValue(mint.supply),
};
}
/**
* Get the total minted supply of this token
* @returns the total supply
*
* @example
* ```jsx
* const supply = await program.totalSupply();
* ```
*/
async totalSupply(): Promise<CurrencyValue> {
const info = await this.getMint();
return toCurrencyValue(info.supply);
}
/**
* Mints the specified amount of new tokens to the connected wallet
* @param amount - The amount of tokens to mint
* @returns the transaction result of the mint
*
* @example
* ```jsx
* // Specify the amount of tokens to mint
* const amount = 1;
* // And then you can mint the tokens
* const tx = await program.mint(amount);
* ```
*/
async mint(amount: Amount): Promise<TransactionResult> {
return this.mintTo(this.metaplex.identity().publicKey.toBase58(), amount);
}
/**
* Mints the specified amount of new tokens to a specific wallet
* @param amount - The amount of tokens to mint
* @returns the transaction result of the mint
*
* @example
* ```jsx
* // Specify the address to mint tokens to
* const address = "{{wallet_address}}"";
* // And the amount of tokens to mint
* const amount = 1;
* // And then you can make a mint transaction
* const tx = await program.mintTo(address, 1);
* ```
*/
async mintTo(
receiverAddress: string,
amount: Amount,
): Promise<TransactionResult> {
const amountParsed = AmountSchema.parse(amount);
const info = await this.getMint();
const result = await this.metaplex.tokens().mint({
amount: token(amountParsed, info.decimals),
mintAddress: this.publicKey,
toOwner: new PublicKey(receiverAddress),
});
return {
signature: result.response.signature,
};
}
/**
* Transfer the specified amount of tokens to another wallet
* @param receiverAddress - The address to send the tokens to
* @param amount - The amount of tokens to send
* @returns the transaction result of the transfer
*
* @example
* ```jsx
* // Specify the address to transfer tokens to
* const to = "...";
* // And the amount of tokens to transfer
* const amount = 1;
* // And then you can make the transfer transaction
* const tx = await program.transfer(to, amount);
* ```
*/
async transfer(
receiverAddress: string,
amount: Amount,
): Promise<TransactionResult> {
const info = await this.getMint();
const result = await this.metaplex.tokens().send({
mintAddress: this.publicKey,
amount: token(amount, info.decimals),
toOwner: new PublicKey(receiverAddress),
});
return {
signature: result.response.signature,
};
}
/**
* Get the token balance of the connected wallet
* @returns the currency value balance
*
* @example
* ```jsx
* const balance = await program.balance();
* console.log(balance.displayValue);
* ```
*/
async balance(): Promise<CurrencyValue> {
return this.balanceOf(this.metaplex.identity().publicKey.toBase58());
}
/**
* Get the token balance of the specified wallet
* @param walletAddress - the wallet address to get the balance of
* @returns the currency value balance
*
* @example
* ```jsx
* const address = "..."
* const balance = await program.balanceOf(address);
* console.log(balance.displayValue);
* ```
*/
async balanceOf(walletAddress: string): Promise<CurrencyValue> {
const mint = await this.getMint();
const addr = await getAssociatedTokenAddress(
this.publicKey,
new PublicKey(walletAddress),
);
try {
const account = await getAccount(this.connection, addr);
return toCurrencyValue({
basisPoints: toBigNumber(account.amount.toString()),
currency: {
symbol: "",
decimals: mint.decimals,
namespace: "spl-token",
},
});
} catch (e) {
throw Error(`No balance found for address '${walletAddress}' - ${e}`);
}
}
private async getMint() {
return await this.metaplex
.tokens()
.findMintByAddress({ address: this.publicKey });
}
}
-22
View File
@@ -1,22 +0,0 @@
import type { NFTCollection } from "./nft-collection";
import type { NFTDrop } from "./nft-drop";
import type { Program } from "./program";
import type { Token } from "./token";
export type ProgramType =
| NFTCollection["accountType"]
| NFTDrop["accountType"]
| Token["accountType"]
| Program["accountType"];
export type PrebuiltProgramMap = {
["nft-collection"]: NFTCollection;
["nft-drop"]: NFTDrop;
["token"]: Token;
};
export type PrebuiltProgramType = keyof PrebuiltProgramMap;
export type ProgramForPrebuiltProgramType<
TProgramType extends PrebuiltProgramType,
> = PrebuiltProgramMap[TProgramType];

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