Compare commits

...
Author SHA1 Message Date
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
78bae497ba Version Packages (#126)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-09-15 14:00:20 -07:00
Joaquim VergesandGitHub 3e425bf8e6 [SOL] get account type for a given address (#132) 2022-09-15 20:54:45 +00:00
Jarrod WattsandGitHub 4dca5b21a6 Change feature detection URLs (#128) 2022-09-15 13:45:04 -07:00
Joaquim VergesandGitHub 42c79e93dc [SDK] fix editing releases from the dashboard (#131) 2022-09-15 19:17:02 +00:00
Joaquim VergesandGitHub 2951b5533c [SOL] New return type for registry.getAccountsForWallet (#130) 2022-09-15 18:09:52 +00:00
Joaquim VergesandGitHub fe8751eeae [SDK] add ownable feature detection (#124) 2022-09-15 17:41:32 +00:00
Jonas DanielsandGitHub a0cd47ec35 [SOL] - explicitly export the missing schemata (#129) 2022-09-15 17:13:20 +00:00
Jarrod WattsandGitHub 3902e7aba4 Update read and write hook names for snippet generation (#127) 2022-09-15 17:36:23 +10:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
8717cb2c05 Version Packages (#118)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-09-14 19:38:16 -07:00
Jarrod WattsandGitHub d57ab03f5d Add useContract hook snippet back in (#125) 2022-09-15 12:34:57 +10:00
Joaquim VergesandGitHub 5aa3c433d7 [Solana] Add sdk.wallet.getBalance() (#123) 2022-09-14 19:27:52 +00:00
Joaquim VergesandGitHub 98dd64a375 [SDK] handle legacy claim method signature in extensions (#122) 2022-09-14 19:15:47 +00:00
Joaquim VergesandGitHub e25dd1ac8e [Solana] Handle WalletAdapter connections, internal cleanup (#120) 2022-09-14 05:04:08 +00:00
Jonas DanielsandGitHub e68f9685b3 fix snippets (#119) 2022-09-14 03:16:58 +00:00
Joaquim VergesandGitHub 84beda1cbb [Solana] Create account registry (#117) 2022-09-13 16:22:00 -07:00
46 changed files with 745 additions and 238 deletions
+14
View File
@@ -1,5 +1,19 @@
# @thirdweb-dev/auth
## 2.0.2
### Patch Changes
- Updated dependencies [[`42c79e9`](https://github.com/thirdweb-dev/js/commit/42c79e93dc958ca46a55d705aeea44ffdbbcc5f6), [`fe8751e`](https://github.com/thirdweb-dev/js/commit/fe8751eeae7ad013b890a8092ddbd091ecbd6708)]:
- @thirdweb-dev/sdk@3.0.2
## 2.0.1
### Patch Changes
- Updated dependencies [[`98dd64a`](https://github.com/thirdweb-dev/js/commit/98dd64a375c302a879aab3c628ecfb84b4dd19da)]:
- @thirdweb-dev/sdk@3.0.1
## 2.0.0
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/auth",
"version": "2.0.0",
"version": "2.0.2",
"exports": {
"./package.json": "./package.json",
"./express": {
+16
View File
@@ -1,5 +1,21 @@
# thirdweb
## 0.7.13
### Patch Changes
- [#128](https://github.com/thirdweb-dev/js/pull/128) [`4dca5b2`](https://github.com/thirdweb-dev/js/commit/4dca5b21a66fe8ff4c4b24663e1b9ad6c446b7e2) Thanks [@jarrodwatts](https://github.com/jarrodwatts)! - Add new documentation for detectable features
- Updated dependencies [[`42c79e9`](https://github.com/thirdweb-dev/js/commit/42c79e93dc958ca46a55d705aeea44ffdbbcc5f6), [`fe8751e`](https://github.com/thirdweb-dev/js/commit/fe8751eeae7ad013b890a8092ddbd091ecbd6708)]:
- @thirdweb-dev/sdk@3.0.2
## 0.7.12
### Patch Changes
- Updated dependencies [[`98dd64a`](https://github.com/thirdweb-dev/js/commit/98dd64a375c302a879aab3c628ecfb84b4dd19da)]:
- @thirdweb-dev/sdk@3.0.1
## 0.7.11
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "thirdweb",
"main": "dist/cli/index.js",
"version": "0.7.11",
"version": "0.7.13",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/cli",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
+2 -2
View File
@@ -70,13 +70,13 @@ export async function detectExtensions(options: any) {
const enabledFeatures: Feature[] = features.enabledFeatures.map(
(feature) => ({
name: feature.name,
reference: `https://portal.thirdweb.com/contracts/${feature.docLinks.contracts}`,
reference: `https://portal.thirdweb.com/extensions/${feature.name.toLowerCase()}`,
}),
);
const suggestedFeatures: Feature[] = features.suggestedFeatures.map(
(feature) => ({
name: feature.name,
reference: `https://portal.thirdweb.com/contracts/${feature.docLinks.contracts}`,
reference: `https://portal.thirdweb.com/extensions/${feature.name.toLowerCase()}`,
}),
);
+14
View File
@@ -1,5 +1,19 @@
# @thirdweb-dev/react
## 3.0.2
### Patch Changes
- Updated dependencies [[`42c79e9`](https://github.com/thirdweb-dev/js/commit/42c79e93dc958ca46a55d705aeea44ffdbbcc5f6), [`fe8751e`](https://github.com/thirdweb-dev/js/commit/fe8751eeae7ad013b890a8092ddbd091ecbd6708)]:
- @thirdweb-dev/sdk@3.0.2
## 3.0.1
### Patch Changes
- Updated dependencies [[`98dd64a`](https://github.com/thirdweb-dev/js/commit/98dd64a375c302a879aab3c628ecfb84b4dd19da)]:
- @thirdweb-dev/sdk@3.0.1
## 3.0.0
### Major Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react",
"version": "3.0.0",
"version": "3.0.2",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
+2 -2
View File
@@ -115,13 +115,13 @@ const NETWORK_INFO_HOOKS = ["useChainId", "useNetwork", "useNetworkMismatch"];
const CUSTOM_CONTRACT_HOOKS = [
"useContract",
"useContractData",
"useContractRead",
"useContractWrite",
"useAllContractEvents",
"useContractEvents",
"useContractType",
"useContractPublishMetadata",
"useContractMetadata",
"useContractCall",
];
const CONTRACT_SETTINGS_HOOKS = [
@@ -134,6 +134,18 @@ export type UseContractResult<
contract: TContract | undefined;
};
/**
* Use this resolve a contract address to a smart contract instance.
*
* @example
* ```javascript
* const { contract, isLoading, error } = useContract("{{contract_address}}");
* ```
*
* @param contractAddress - the address of the deployed contract
* @returns a response object that includes the contract once it is resolved
* @public
*/
export function useContract<
TContract extends ValidContractInstance = SmartContract,
>(contractAddress: RequiredParam<ContractAddress>) {
+3
View File
@@ -0,0 +1,3 @@
docs/
etc/
temp/
+14
View File
@@ -1,5 +1,19 @@
# @thirdweb-dev/sdk
## 3.0.2
### Patch Changes
- [#131](https://github.com/thirdweb-dev/js/pull/131) [`42c79e9`](https://github.com/thirdweb-dev/js/commit/42c79e93dc958ca46a55d705aeea44ffdbbcc5f6) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix editing releases from the dashboard
- [#124](https://github.com/thirdweb-dev/js/pull/124) [`fe8751e`](https://github.com/thirdweb-dev/js/commit/fe8751eeae7ad013b890a8092ddbd091ecbd6708) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Add feature detection for Ownable
## 3.0.1
### Patch Changes
- [#122](https://github.com/thirdweb-dev/js/pull/122) [`98dd64a`](https://github.com/thirdweb-dev/js/commit/98dd64a375c302a879aab3c628ecfb84b4dd19da) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - handle legacy claim method signature in extensions
## 3.0.0
### Major Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/sdk",
"version": "3.0.0",
"version": "3.0.2",
"description": "The main thirdweb SDK.",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/sdk",
"license": "Apache-2.0",
@@ -20,7 +20,7 @@
"build": "tsc && preconstruct build",
"test:all": "SWC_NODE_PROJECT=./tsconfig.test.json mocha --timeout 30000 --parallel './test/**/*.test.ts'",
"test": "make test",
"test:single": "SWC_NODE_PROJECT=./tsconfig.test.json mocha --timeout 30000 --"
"test:single": "SWC_NODE_PROJECT=./tsconfig.test.json mocha --timeout 30000"
},
"devDependencies": {
"@esbuild-plugins/node-modules-polyfill": "^0.1.4",
+3 -5
View File
@@ -65,14 +65,12 @@ const additionalClassesToParse = [
"ContractPlatformFee",
"WalletAuthenticator",
"UserWallet",
"SmartContract",
];
const modules = json.members[0].members.filter(
(m) =>
(m.kind === "Class" &&
m.members
.filter((cMember) => cMember.kind === "Property")
.findIndex((property) => property.name === "contractType") > -1) ||
(m.kind === "Class" && m.name.endsWith("Impl")) ||
additionalClassesToParse.includes(m.name),
);
@@ -154,7 +152,7 @@ const moduleMap = modules.reduce((acc, m) => {
const baseClassMembers = baseClassCode
? parseMembers(baseClassCode.members, "Method", baseClassCode.name)
: [];
acc[m.name] = {
acc[m.name.replace("Impl", "")] = {
name: m.name,
summary: Formatter.renderDocNode(docComment.summarySection),
remarks: docComment.remarksBlock
@@ -33,6 +33,7 @@ import {
} from "./erc1155-features";
import {
FEATURE_METADATA,
FEATURE_OWNER,
FEATURE_PERMISSIONS,
FEATURE_PLATFORM_FEE,
FEATURE_PRIMARY_SALE,
@@ -74,7 +75,8 @@ export type Feature =
| typeof FEATURE_PLATFORM_FEE
| typeof FEATURE_PRIMARY_SALE
| typeof FEATURE_PERMISSIONS
| typeof FEATURE_METADATA;
| typeof FEATURE_METADATA
| typeof FEATURE_OWNER;
/**
* @internal
@@ -100,4 +102,5 @@ export const SUPPORTED_FEATURES: Record<string, Feature> = {
[FEATURE_PRIMARY_SALE.name]: FEATURE_PRIMARY_SALE,
[FEATURE_PERMISSIONS.name]: FEATURE_PERMISSIONS,
[FEATURE_METADATA.name]: FEATURE_METADATA,
[FEATURE_OWNER.name]: FEATURE_OWNER,
};
@@ -3,6 +3,7 @@ import IPermissionsAbi from "@thirdweb-dev/contracts-js/dist/abis/IPermissions.j
import IThirdwebPlatformFeeAbi from "@thirdweb-dev/contracts-js/dist/abis/IPlatformFee.json";
import IThirdwebPrimarySaleAbi from "@thirdweb-dev/contracts-js/dist/abis/IPrimarySale.json";
import IThirdwebRoyaltyAbi from "@thirdweb-dev/contracts-js/dist/abis/IRoyalty.json";
import IOwnableAbi from "@thirdweb-dev/contracts-js/dist/abis/Ownable.json";
export const FEATURE_ROYALTY = {
name: "Royalty",
@@ -58,3 +59,14 @@ export const FEATURE_METADATA = {
abis: [IContractMetadataAbi],
features: {},
} as const;
export const FEATURE_OWNER = {
name: "Ownable",
namespace: "owner",
docLinks: {
sdk: "sdk.owner",
contracts: "Ownable",
},
abis: [IOwnableAbi],
features: {},
} as const;
@@ -281,25 +281,12 @@ export class EditionDropImpl extends StandardErc1155<DropERC1155> {
quantity: BigNumberish,
checkERC20Allowance = true, // TODO split up allowance checks
): Promise<TransactionTask> {
const claimVerification = await this.erc1155.claimConditions.prepareClaim(
return this.erc1155.getClaimTransaction(
destinationAddress,
tokenId,
quantity,
checkERC20Allowance,
{ checkERC20Allowance },
);
return TransactionTask.make({
contractWrapper: this.contractWrapper,
functionName: "claim",
args: [
destinationAddress,
tokenId,
quantity,
claimVerification.currencyAddress,
claimVerification.price,
claimVerification.proofs,
claimVerification.maxQuantityPerTransaction,
],
overrides: claimVerification.overrides,
});
}
/**
@@ -331,13 +318,9 @@ export class EditionDropImpl extends StandardErc1155<DropERC1155> {
quantity: BigNumberish,
checkERC20Allowance = true,
): Promise<TransactionResult> {
const tx = await this.getClaimTransaction(
destinationAddress,
tokenId,
quantity,
return this.erc1155.claimTo(destinationAddress, tokenId, quantity, {
checkERC20Allowance,
);
return await tx.execute();
});
}
/**
@@ -440,24 +440,10 @@ export class NFTDropImpl extends StandardErc721<DropERC721> {
public async getClaimTransaction(
destinationAddress: string,
quantity: BigNumberish,
checkERC20Allowance = true, // TODO split up allowance checks
checkERC20Allowance = true,
): Promise<TransactionTask> {
const claimVerification = await this.prepareClaim(
quantity,
return this.erc721.getClaimTransaction(destinationAddress, quantity, {
checkERC20Allowance,
);
return TransactionTask.make({
contractWrapper: this.contractWrapper,
functionName: "claim",
args: [
destinationAddress,
quantity,
claimVerification.currencyAddress,
claimVerification.price,
claimVerification.proofs,
claimVerification.maxQuantityPerTransaction,
],
overrides: claimVerification.overrides,
});
}
@@ -488,27 +474,9 @@ export class NFTDropImpl extends StandardErc721<DropERC721> {
quantity: BigNumberish,
checkERC20Allowance = true,
): Promise<TransactionResultWithId<NFTMetadataOwner>[]> {
const task = await this.getClaimTransaction(
destinationAddress,
quantity,
return this.erc721.claimTo(destinationAddress, quantity, {
checkERC20Allowance,
);
const { receipt } = await task.execute();
const event = this.contractWrapper.parseLogs<TokensClaimedEvent>(
"TokensClaimed",
receipt?.logs,
);
const startingIndex: BigNumber = event[0].args.startTokenId;
const endingIndex = startingIndex.add(quantity);
const results: TransactionResultWithId<NFTMetadataOwner>[] = [];
for (let id = startingIndex; id.lt(endingIndex); id = id.add(1)) {
results.push({
id,
receipt,
data: () => this.get(id),
});
}
return results;
});
}
/**
+59 -27
View File
@@ -2,7 +2,18 @@ import { ALL_ROLES, assertEnabled, detectContractFeature } from "../common";
import { FEATURE_TOKEN } from "../constants/erc20-features";
import { FEATURE_NFT } from "../constants/erc721-features";
import { FEATURE_EDITION } from "../constants/erc1155-features";
import { ContractEncoder, NetworkOrSignerOrProvider } from "../core";
import {
FEATURE_OWNER,
FEATURE_PERMISSIONS,
FEATURE_PLATFORM_FEE,
FEATURE_PRIMARY_SALE,
FEATURE_ROYALTY,
} from "../constants/thirdweb-features";
import {
ContractEncoder,
ContractOwner,
NetworkOrSignerOrProvider,
} from "../core";
import { ContractEvents } from "../core/classes/contract-events";
import { ContractInterceptor } from "../core/classes/contract-interceptor";
import { ContractMetadata } from "../core/classes/contract-metadata";
@@ -25,6 +36,7 @@ import type {
IPlatformFee,
IPrimarySale,
IRoyalty,
Ownable,
} from "@thirdweb-dev/contracts-js";
import { IStorage } from "@thirdweb-dev/storage";
import { BaseContract, CallOverrides, ContractInterface } from "ethers";
@@ -44,10 +56,10 @@ import { BaseContract, CallOverrides, ContractInterface } from "ethers";
* await contract.call("myCustomFunction", param1, param2);
*
* // if your contract follows the ERC721 standard, contract.nft will be present
* const allNFTs = await contract.nft.query.all()
* const allNFTs = await contract.erc721.query.all()
*
* // if your contract extends IMintableERC721, contract.nft.mint() will be available
* const tx = await contract.nft.mint({
* const tx = await contract.erc721.mint({
* name: "Cool NFT",
* image: readFileSync("some_image.png"),
* });
@@ -60,7 +72,6 @@ export class SmartContract<TContract extends BaseContract = BaseContract>
{
private contractWrapper;
private storage;
private options;
// utilities
public events: ContractEvents<TContract>;
@@ -69,37 +80,62 @@ export class SmartContract<TContract extends BaseContract = BaseContract>
public estimator: GasCostEstimator<TContract>;
public publishedMetadata: ContractPublishedMetadata<TContract>;
public abi: ContractInterface;
// features
public metadata: ContractMetadata<BaseContract, any>;
public royalties: ContractRoyalty<IRoyalty, any> | undefined;
public roles: ContractRoles<IPermissions, any> | undefined;
public sales: ContractPrimarySale<IPrimarySale> | undefined;
public platformFees: ContractPlatformFee<IPlatformFee> | undefined;
private token: Erc20 | undefined;
private nft: Erc721 | undefined;
private edition: Erc1155 | undefined;
/**
* Handle royalties
*/
get royalties(): ContractRoyalty<IRoyalty, any> {
return assertEnabled(this.detectRoyalties(), FEATURE_ROYALTY);
}
/**
* Handle permissions
*/
get roles(): ContractRoles<IPermissions, any> {
return assertEnabled(this.detectRoles(), FEATURE_PERMISSIONS);
}
/**
* Handle primary sales
*/
get sales(): ContractPrimarySale<IPrimarySale> {
return assertEnabled(this.detectPrimarySales(), FEATURE_PRIMARY_SALE);
}
/**
* Handle platform fees
*/
get platformFees(): ContractPlatformFee<IPlatformFee> {
return assertEnabled(this.detectPlatformFees(), FEATURE_PLATFORM_FEE);
}
/**
* Set and get the owner of the contract
*/
get owner(): ContractOwner<Ownable> {
return assertEnabled(this.detectOwnable(), FEATURE_OWNER);
}
/**
* Auto-detects ERC20 standard functions.
*/
get erc20(): Erc20 {
return assertEnabled(this.token, FEATURE_TOKEN);
return assertEnabled(this.detectErc20(), FEATURE_TOKEN);
}
/**
* Auto-detects ERC721 standard functions.
*/
get erc721(): Erc721 {
return assertEnabled(this.nft, FEATURE_NFT);
return assertEnabled(this.detectErc721(), FEATURE_NFT);
}
/**
* Auto-detects ERC1155 standard functions.
*/
get erc1155(): Erc1155 {
return assertEnabled(this.edition, FEATURE_EDITION);
return assertEnabled(this.detectErc1155(), FEATURE_EDITION);
}
constructor(
@@ -115,7 +151,6 @@ export class SmartContract<TContract extends BaseContract = BaseContract>
options,
),
) {
this.options = options;
this.storage = storage;
this.contractWrapper = contractWrapper;
this.abi = abi;
@@ -134,16 +169,6 @@ export class SmartContract<TContract extends BaseContract = BaseContract>
CustomContractSchema,
this.storage,
);
// feature detection
this.royalties = this.detectRoyalties();
this.roles = this.detectRoles();
this.sales = this.detectPrimarySales();
this.platformFees = this.detectPlatformFees();
this.token = this.detectErc20();
this.nft = this.detectErc721();
this.edition = this.detectErc1155();
}
onNetworkUpdated(network: NetworkOrSignerOrProvider): void {
@@ -247,4 +272,11 @@ export class SmartContract<TContract extends BaseContract = BaseContract>
}
return undefined;
}
private detectOwnable() {
if (detectContractFeature<Ownable>(this.contractWrapper, "Ownable")) {
return new ContractOwner(this.contractWrapper);
}
return undefined;
}
}
@@ -1,5 +1,6 @@
import {
detectContractFeature,
ExtensionNotImplementedError,
fetchContractMetadataFromAddress,
hasFunction,
} from "../../common";
@@ -141,7 +142,7 @@ export class ContractMetadata<
z.output<TSchema["output"]>
>;
} else {
throw new Error("Contract does not support updating contract metadata");
throw new ExtensionNotImplementedError(FEATURE_METADATA);
}
}
@@ -0,0 +1,39 @@
import { FEATURE_OWNER } from "../../constants/thirdweb-features";
import { DetectableFeature } from "../interfaces/DetectableFeature";
import { TransactionResult } from "../types";
import { ContractWrapper } from "./contract-wrapper";
import type { Ownable } from "@thirdweb-dev/contracts-js";
/**
* Encodes and decodes Contract functions
* @public
*/
export class ContractOwner<TContract extends Ownable>
implements DetectableFeature
{
featureName = FEATURE_OWNER.name;
private contractWrapper;
constructor(contractWrapper: ContractWrapper<TContract>) {
this.contractWrapper = contractWrapper;
}
/**
* Return the current owner of the contract
* @returns the owner address
*/
public async get(): Promise<string> {
return this.contractWrapper.readContract.owner();
}
/**
* set tje new owner of the contract
*/
public async set(address: string): Promise<TransactionResult> {
return {
receipt: await this.contractWrapper.sendTransaction("setOwner", [
address,
]),
};
}
}
@@ -333,7 +333,10 @@ export class ContractPublisher extends RPCConnectionHandler {
const fullMetadata = FullPublishMetadataSchemaInput.parse({
...extraMetadata,
...predeployMetadata,
metadataUri: predeployMetadata.metadataUri,
bytecodeUri: predeployMetadata.bytecodeUri,
name: predeployMetadata.name,
analytics: predeployMetadata.analytics,
publisher,
});
const fullMetadataUri = await this.storage.uploadMetadata(fullMetadata);
@@ -1,6 +1,10 @@
import { hasFunction } from "../../common";
import { FEATURE_EDITION_CLAIMABLE_WITH_CONDITIONS } from "../../constants/erc1155-features";
import { CustomContractSchema } from "../../schema/contracts/custom";
import { ClaimOptions } from "../../types/claim-conditions/claim-conditions";
import {
ClaimOptions,
ClaimVerification,
} from "../../types/claim-conditions/claim-conditions";
import { BaseClaimConditionERC1155 } from "../../types/eips";
import { DetectableFeature } from "../interfaces/DetectableFeature";
import { TransactionResult } from "../types";
@@ -8,6 +12,7 @@ import { TransactionTask } from "./TransactionTask";
import { ContractMetadata } from "./contract-metadata";
import { ContractWrapper } from "./contract-wrapper";
import { DropErc1155ClaimConditions } from "./drop-erc1155-claim-conditions";
import { DropERC1155 } from "@thirdweb-dev/contracts-js";
import { IStorage } from "@thirdweb-dev/storage";
import { BigNumberish, ethers } from "ethers";
@@ -73,18 +78,12 @@ export class Erc1155ClaimableWithConditions implements DetectableFeature {
return TransactionTask.make({
contractWrapper: this.contractWrapper,
functionName: "claim",
args: [
destinationAddress,
args: await this.getArgs(
tokenId,
destinationAddress,
quantity,
claimVerification.currencyAddress,
claimVerification.price,
{
proof: claimVerification.proofs,
maxQuantityInAllowlist: claimVerification.maxQuantityPerTransaction,
},
ethers.utils.toUtf8Bytes(""),
],
claimVerification,
),
overrides: claimVerification.overrides,
});
}
@@ -124,4 +123,51 @@ export class Erc1155ClaimableWithConditions implements DetectableFeature {
);
return await tx.execute();
}
private async getArgs(
tokenId: BigNumberish,
destinationAddress: string,
quantity: BigNumberish,
claimVerification: ClaimVerification,
): Promise<any[]> {
const isLegacyNFTContract = await this.isLegacyNFTContract();
if (isLegacyNFTContract) {
return [
destinationAddress,
tokenId,
quantity,
claimVerification.currencyAddress,
claimVerification.price,
claimVerification.proofs,
claimVerification.maxQuantityPerTransaction,
];
}
return [
destinationAddress,
tokenId,
quantity,
claimVerification.currencyAddress,
claimVerification.price,
{
proof: claimVerification.proofs,
maxQuantityInAllowlist: claimVerification.maxQuantityPerTransaction,
},
ethers.utils.toUtf8Bytes(""),
];
}
private async isLegacyNFTContract() {
if (hasFunction<DropERC1155>("contractType", this.contractWrapper)) {
try {
const contractType = ethers.utils.toUtf8String(
await this.contractWrapper.readContract.contractType(),
);
return contractType.includes("DropERC1155");
} catch (e) {
return false;
}
} else {
return false;
}
}
}
@@ -1,7 +1,8 @@
import { hasFunction } from "../../common";
import { FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS } from "../../constants/erc721-features";
import { NFTMetadataOwner } from "../../schema";
import { CustomContractSchema } from "../../schema/contracts/custom";
import { ClaimOptions } from "../../types";
import { ClaimOptions, ClaimVerification } from "../../types";
import { BaseClaimConditionERC721 } from "../../types/eips";
import { DetectableFeature } from "../interfaces/DetectableFeature";
import { TransactionResultWithId } from "../types";
@@ -10,6 +11,7 @@ import { ContractMetadata } from "./contract-metadata";
import { ContractWrapper } from "./contract-wrapper";
import { DropClaimConditions } from "./drop-claim-conditions";
import { Erc721 } from "./erc-721";
import type { DropERC721 } from "@thirdweb-dev/contracts-js";
import { TokensClaimedEvent } from "@thirdweb-dev/contracts-js/dist/declarations/src/Drop";
import { IStorage } from "@thirdweb-dev/storage";
import { BigNumber, BigNumberish, ethers } from "ethers";
@@ -96,20 +98,11 @@ export class Erc721ClaimableWithConditions implements DetectableFeature {
quantity,
options?.checkERC20Allowance || true,
);
return TransactionTask.make({
contractWrapper: this.contractWrapper,
functionName: "claim",
args: [
destinationAddress,
quantity,
claimVerification.currencyAddress,
claimVerification.price,
{
proof: claimVerification.proofs,
maxQuantityInAllowlist: claimVerification.maxQuantityPerTransaction,
},
ethers.utils.toUtf8Bytes(""),
],
args: await this.getArgs(destinationAddress, quantity, claimVerification),
overrides: claimVerification.overrides,
});
}
@@ -161,4 +154,48 @@ export class Erc721ClaimableWithConditions implements DetectableFeature {
}
return results;
}
private async getArgs(
destinationAddress: string,
quantity: BigNumberish,
claimVerification: ClaimVerification,
): Promise<any[]> {
const isLegacyNFTContract = await this.isLegacyNFTContract();
if (isLegacyNFTContract) {
return [
destinationAddress,
quantity,
claimVerification.currencyAddress,
claimVerification.price,
claimVerification.proofs,
claimVerification.maxQuantityPerTransaction,
];
}
return [
destinationAddress,
quantity,
claimVerification.currencyAddress,
claimVerification.price,
{
proof: claimVerification.proofs,
maxQuantityInAllowlist: claimVerification.maxQuantityPerTransaction,
},
ethers.utils.toUtf8Bytes(""),
];
}
private async isLegacyNFTContract() {
if (hasFunction<DropERC721>("contractType", this.contractWrapper)) {
try {
const contractType = ethers.utils.toUtf8String(
await this.contractWrapper.readContract.contractType(),
);
return contractType.includes("DropERC721");
} catch (e) {
return false;
}
} else {
return false;
}
}
}
+1
View File
@@ -41,3 +41,4 @@ export * from "./contract-events";
export * from "./contract-interceptor";
export * from "./contract-platform-fee";
export * from "./contract-published-metadata";
export * from "./contract-owner";
+14
View File
@@ -33,3 +33,17 @@ export * from "./enums";
export * from "./common";
export * from "./constants";
export * from "./contracts";
// explcitly export the *TYPES* of prebuilt contracts
export type { EditionImpl } from "./contracts/prebuilt-implementations/edition";
export type { EditionDropImpl } from "./contracts/prebuilt-implementations/edition-drop";
export type { MarketplaceImpl } from "./contracts/prebuilt-implementations/marketplace";
export type { MultiwrapImpl } from "./contracts/prebuilt-implementations/multiwrap";
export type { NFTCollectionImpl } from "./contracts/prebuilt-implementations/nft-collection";
export type { NFTDropImpl } from "./contracts/prebuilt-implementations/nft-drop";
export type { PackImpl } from "./contracts/prebuilt-implementations/pack";
export type { SignatureDropImpl } from "./contracts/prebuilt-implementations/signature-drop";
export type { SplitImpl } from "./contracts/prebuilt-implementations/split";
export type { TokenImpl } from "./contracts/prebuilt-implementations/token";
export type { TokenDropImpl } from "./contracts/prebuilt-implementations/token-drop";
export type { VoteImpl } from "./contracts/prebuilt-implementations/vote";
+9 -2
View File
@@ -32,7 +32,7 @@ describe("Custom Contracts", async () => {
let sdk: ThirdwebSDK;
let simpleContractUri: string;
beforeAll(async () => {
before(async () => {
[adminWallet, samWallet, bobWallet] = signers;
sdk = new ThirdwebSDK(adminWallet);
simpleContractUri =
@@ -227,7 +227,14 @@ describe("Custom Contracts", async () => {
const c = await sdk.getContractFromAbi("", VoteERC20__factory.abi);
invariant(c, "Contract undefined");
invariant(c.metadata, "Metadata undefined");
expect(c.roles).to.eq(undefined);
try {
c.roles.get("admin");
} catch (e) {
expectError(
e,
"contract does not implement the 'PermissionsEnumerable' Extension",
);
}
});
it("should detect feature: erc20", async () => {
+20
View File
@@ -1,5 +1,25 @@
# @thirdweb-dev/solana
## 0.2.10
### Patch Changes
- [#129](https://github.com/thirdweb-dev/js/pull/129) [`a0cd47e`](https://github.com/thirdweb-dev/js/commit/a0cd47ec3596d23f974e58d668ce02e34bfe3528) Thanks [@jnsdls](https://github.com/jnsdls)! - explicitly export the program schemata from the package
- [#132](https://github.com/thirdweb-dev/js/pull/132) [`3e425bf`](https://github.com/thirdweb-dev/js/commit/3e425bf8e65db99fc4043ea5d2bc4b863eabce52) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Get account type for a given address
- [#130](https://github.com/thirdweb-dev/js/pull/130) [`2951b55`](https://github.com/thirdweb-dev/js/commit/2951b5533c13d1d7633c491df7e5710b0a6608dd) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - New return type for `registry.getAccountsForWallet`
## 0.2.9
### Patch Changes
- [#120](https://github.com/thirdweb-dev/js/pull/120) [`e25dd1a`](https://github.com/thirdweb-dev/js/commit/e25dd1ac8e5ef25d844b2d570ac557b0440e6e51) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Proper handling of WalletAdapter connections
- [#117](https://github.com/thirdweb-dev/js/pull/117) [`84beda1`](https://github.com/thirdweb-dev/js/commit/84beda1cbb17f1e5ffb1edbea2acb7c42f47fbab) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Account registry implementation
- [#123](https://github.com/thirdweb-dev/js/pull/123) [`5aa3c43`](https://github.com/thirdweb-dev/js/commit/5aa3c433d790a2f65515758171f5822864826367) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Add ability to fetch native balance for the connected wallet
## 0.2.8
### Patch Changes
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/solana",
"version": "0.2.8",
"version": "0.2.10",
"description": "thirdweb Solana SDK",
"main": "dist/thirdweb-dev-solana.cjs.js",
"module": "dist/thirdweb-dev-solana.esm.js",
@@ -61,6 +61,7 @@
"@solana/spl-token": "^0.2.0",
"@solana/web3.js": "^1.50.1",
"@thirdweb-dev/storage": "*",
"bn.js": "^5.2.1",
"eventemitter3": "^4.0.7",
"form-data": "^4.0.0",
"formdata-polyfill": "^4.0.10",
+24 -16
View File
@@ -7,7 +7,6 @@ import {
NFTDropContractSchema,
NFTDropMetadataInput,
} from "../types/contracts/nft-drop";
import { UserWallet } from "./user-wallet";
import { findMetadataPda, Metaplex, token } from "@metaplex-foundation/js";
import {
createCreateMetadataAccountV2Instruction,
@@ -15,24 +14,21 @@ import {
} from "@metaplex-foundation/mpl-token-metadata";
import { Keypair, PublicKey } from "@solana/web3.js";
import { IStorage } from "@thirdweb-dev/storage";
import invariant from "tiny-invariant";
export class Deployer {
private wallet: UserWallet;
private metaplex: Metaplex;
private storage: IStorage;
constructor(metaplex: Metaplex, wallet: UserWallet, storage: IStorage) {
constructor(metaplex: Metaplex, storage: IStorage) {
this.metaplex = metaplex;
this.wallet = wallet;
this.storage = storage;
}
async createToken(tokenMetadata: TokenMetadataInput): Promise<string> {
const tokenMetadataParsed = TokenMetadataInputSchema.parse(tokenMetadata);
const uri = await this.storage.uploadMetadata(tokenMetadataParsed);
const owner = this.wallet.getSigner().publicKey;
const mint = Keypair.generate();
const owner = this.metaplex.identity().publicKey;
const mintTx = await this.metaplex
.tokens()
.builders()
@@ -43,9 +39,6 @@ export class Deployer {
tokenMetadataParsed.decimals,
),
mint,
mintAuthority: this.wallet.getSigner(),
payer: this.wallet.getSigner(),
owner,
});
const data: DataV2 = {
@@ -75,7 +68,7 @@ export class Deployer {
{ createMetadataAccountArgsV2: { data, isMutable: false } },
);
await mintTx
.add({ instruction: metaTx, signers: [this.wallet.getSigner()] })
.add({ instruction: metaTx, signers: [this.metaplex.identity()] })
.sendAndConfirm(this.metaplex);
return mint.publicKey.toBase58();
@@ -94,9 +87,6 @@ export class Deployer {
sellerFeeBasisPoints: 0,
uri,
isCollection: true,
collectionAuthority: this.wallet.getSigner(),
updateAuthority: this.wallet.getSigner(),
mintAuthority: this.wallet.getSigner(),
creators: collectionMetadata.creators?.map((creator) => ({
address: new PublicKey(creator.address),
share: creator.share,
@@ -107,13 +97,31 @@ export class Deployer {
}
async createNftDrop(metadata: NFTDropMetadataInput): Promise<string> {
invariant(this.wallet.signer, "Wallet is not connected");
const parsed = NFTDropContractSchema.parse(metadata);
// TODO create a collection with metadata and associate it with the drop
// TODO make it a single tx
const collection = await this.createNftCollection(metadata);
const creators =
parsed.creators.length > 0
? parsed.creators.map((creator) => ({
address: new PublicKey(creator.address),
share: creator.share,
verified: creator.verified,
}))
: [
{
address: this.metaplex.identity().publicKey,
share: 100,
verified: true,
},
];
const { candyMachine: nftDrop } = await this.metaplex
.candyMachines()
.create({ ...parsed })
.create({
...parsed,
collection: new PublicKey(collection),
creators,
})
.run();
return nftDrop.address.toBase58();
+115
View File
@@ -0,0 +1,115 @@
import { WalletAccount } from "../types/common";
import {
CandyMachine,
Metadata,
Metaplex,
TokenMetadataProgram,
} from "@metaplex-foundation/js";
import { TokenStandard } from "@metaplex-foundation/mpl-token-metadata";
import { PublicKey } from "@solana/web3.js";
export class Registry {
private metaplex: Metaplex;
constructor(metaplex: Metaplex) {
this.metaplex = metaplex;
}
public async getAccountType(address: string) {
try {
const metadata = await this.metaplex
.nfts()
.findByMint({ mintAddress: new PublicKey(address) })
.run();
if (metadata) {
if (metadata.collectionDetails) {
return "nft-collection";
} else {
if (metadata.tokenStandard === TokenStandard.Fungible) {
return "token";
}
}
}
} catch (e) {
try {
const candyMachine = await this.metaplex
.candyMachines()
.findByAddress({ address: new PublicKey(address) })
.run();
if (candyMachine) {
return "nft-drop";
}
} catch (err) {
return undefined;
}
}
return undefined;
}
public async getAccountsForWallet(
walletAddress: string,
): Promise<WalletAccount[]> {
const pubKeys = await this.getMetadataAddressesForWallet(walletAddress);
const metadatas = await this.metaplex
.nfts()
.findAllByMintList({ mints: pubKeys })
.run();
const candyMachines = await this.metaplex
.candyMachines()
.findAllBy({
type: "wallet",
publicKey: new PublicKey(walletAddress),
})
.run();
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 getDropForCollection(candyMachines: CandyMachine[], meta: Metadata) {
return candyMachines.find(
(candyMachine) =>
candyMachine.collectionMintAddress?.toBase58() ===
meta.mintAddress.toBase58(),
);
}
public async getMetadataAddressesForWallet(walletAddress: string) {
return await TokenMetadataProgram.metadataV1Accounts(this.metaplex)
.selectMint()
.whereCreator(1, new PublicKey(walletAddress))
.getDataAsPublicKeys();
}
}
+43 -8
View File
@@ -1,5 +1,17 @@
// import { getPayer } from "../utils/local-config";
import { Signer } from "@metaplex-foundation/js";
import { CurrencyValue, WalletSigner } from "../types/common";
import { toCurrencyValue } from "../utils/token";
import {
guestIdentity,
isIdentitySigner,
isKeypairSigner,
keypairIdentity,
Metaplex,
Signer,
sol,
walletAdapterIdentity,
} from "@metaplex-foundation/js";
import { Keypair } from "@solana/web3.js";
import EventEmitter from "eventemitter3";
import invariant from "tiny-invariant";
@@ -13,7 +25,7 @@ export interface UserWalletEvents {
/**
* Emitted when `sdk.wallet.connect()` is called.
*/
connected: [Signer];
connected: [WalletSigner];
/**
* Emitted when `sdk.wallet.disconnect()` is called.
*/
@@ -23,23 +35,46 @@ export interface UserWalletEvents {
export class UserWallet {
public signer: Signer | undefined;
public events = new EventEmitter<UserWalletEvents>();
private metaplex: Metaplex;
public connect(wallet: Signer) {
this.signer = wallet;
constructor(metaplex: Metaplex) {
this.metaplex = metaplex;
}
public connect(wallet: WalletSigner) {
this.connectToMetaplex(wallet);
this.events.emit("connected", wallet);
}
public disconnect() {
this.signer = undefined;
// TODO implement our own read only identity plugin with our own error messages
this.metaplex.use(guestIdentity());
this.events.emit("disconnected");
}
public getAddress() {
return this.signer?.publicKey.toBase58();
return this.metaplex.identity().publicKey.toBase58();
}
public getSigner() {
invariant(this.signer, "Wallet is not connected");
return this.signer;
return this.metaplex.identity();
}
public async getBalance(): Promise<CurrencyValue> {
const value = await this.metaplex.connection.getBalance(
this.metaplex.identity().publicKey,
);
return toCurrencyValue(sol(value));
}
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,5 +1,4 @@
import { NFTHelper } from "../classes/helpers/nft-helper";
import { UserWallet } from "../classes/user-wallet";
import { METAPLEX_PROGRAM_ID } from "../constants/addresses";
import { TransactionResult } from "../types/common";
import {
@@ -20,7 +19,6 @@ import { ConfirmedSignatureInfo, PublicKey } from "@solana/web3.js";
import { IStorage } from "@thirdweb-dev/storage";
export class NFTCollection {
private wallet: UserWallet;
private metaplex: Metaplex;
private storage: IStorage;
private nft: NFTHelper;
@@ -29,10 +27,8 @@ export class NFTCollection {
constructor(
collectionMintAddress: string,
metaplex: Metaplex,
wallet: UserWallet,
storage: IStorage,
) {
this.wallet = wallet;
this.storage = storage;
this.metaplex = metaplex;
this.nft = new NFTHelper(metaplex);
@@ -213,11 +209,12 @@ export class NFTCollection {
const { nft } = await this.metaplex
.nfts()
.create({
// useExistingMint: newMint,
name: metadata.name || "",
uri,
sellerFeeBasisPoints: 0,
collection: this.collectionMintAddress,
collectionAuthority: this.wallet.signer,
collectionAuthority: this.metaplex.identity(),
tokenOwner: new PublicKey(to),
// Always sets max supply to unlimited so editions can be minted
maxSupply: null,
+19 -13
View File
@@ -1,36 +1,42 @@
import { ClaimConditions } from "../classes/claim-conditions";
import { NFTHelper } from "../classes/helpers/nft-helper";
import { UserWallet } from "../classes/user-wallet";
import { TransactionResult } from "../types/common";
import { CommonNFTInput, NFTMetadata, NFTMetadataInput } from "../types/nft";
import {
CommonNFTInput,
NFTCollectionMetadata,
NFTMetadata,
NFTMetadataInput,
} from "../types/nft";
import { Metaplex } from "@metaplex-foundation/js";
import { Connection, PublicKey } from "@solana/web3.js";
import { PublicKey } from "@solana/web3.js";
import { IStorage } from "@thirdweb-dev/storage";
import invariant from "tiny-invariant";
export class NFTDrop {
private connection: Connection;
private wallet: UserWallet;
private metaplex: Metaplex;
private storage: IStorage;
private nft: NFTHelper;
public dropMintAddress: PublicKey;
public claimConditions: ClaimConditions;
constructor(
dropMintAddress: string,
metaplex: Metaplex,
wallet: UserWallet,
storage: IStorage,
) {
this.wallet = wallet;
constructor(dropMintAddress: string, metaplex: Metaplex, storage: IStorage) {
this.storage = storage;
this.metaplex = metaplex;
this.connection = metaplex.connection;
this.nft = new NFTHelper(metaplex);
this.dropMintAddress = new PublicKey(dropMintAddress);
this.claimConditions = new ClaimConditions(dropMintAddress, metaplex);
}
async getMetadata(): Promise<NFTCollectionMetadata> {
const info = await this.getCandyMachine();
invariant(info.collectionMintAddress, "Collection mint address not found");
const metadata = await this.metaplex
.nfts()
.findByMint({ mintAddress: info.collectionMintAddress })
.run();
return this.nft.toNFTMetadata(metadata);
}
async get(mintAddress: string): Promise<NFTMetadata> {
return this.nft.get(mintAddress);
}
+1 -1
View File
@@ -8,7 +8,7 @@ import { Signer } from "@solana/web3.js";
export class Program {
private program: AnchorProgram<Idl>;
constructor(programAddress: string, idl: Idl, provider: AnchorProvider) {
constructor(programAddress: string, idl: Idl, provider?: AnchorProvider) {
this.program = new AnchorProgram(idl, programAddress, provider);
}
+27 -19
View File
@@ -1,9 +1,15 @@
import { UserWallet } from "../classes/user-wallet";
import { Amount, AmountSchema, TransactionResult } from "../types/common";
import {
Amount,
AmountSchema,
CurrencyValue,
TransactionResult,
} from "../types/common";
import { TokenMetadata } from "../types/nft";
import { toCurrencyValue } from "../utils/token";
import {
findMetadataPda,
Metaplex,
toBigNumber,
token,
toMetadata,
toMetadataAccount,
@@ -14,18 +20,11 @@ import { IStorage } from "@thirdweb-dev/storage";
export class Token {
private connection: Connection;
private wallet: UserWallet;
private metaplex: Metaplex;
private storage: IStorage;
tokenMintAddress: PublicKey;
constructor(
tokenMintAddress: string,
metaplex: Metaplex,
wallet: UserWallet,
storage: IStorage,
) {
this.wallet = wallet;
constructor(tokenMintAddress: string, metaplex: Metaplex, storage: IStorage) {
this.storage = storage;
this.metaplex = metaplex;
this.connection = metaplex.connection;
@@ -40,6 +39,7 @@ export class Token {
}
async getMetadata(): Promise<TokenMetadata> {
const mint = await this.getMint();
const addr = findMetadataPda(this.tokenMintAddress);
const account = await this.metaplex.rpc().getAccount(addr);
const meta = toMetadata(toMetadataAccount(account));
@@ -48,15 +48,15 @@ export class Token {
uri: meta.uri,
name: meta.name,
symbol: meta.symbol,
decimals: mint.decimals,
supply: toCurrencyValue(mint.supply),
...meta.json,
} as TokenMetadata;
}
async totalSupply(): Promise<bigint> {
async totalSupply(): Promise<CurrencyValue> {
const info = await this.getMint();
const value = BigInt(info.supply.basisPoints.toString());
// TODO use CurrencyValue to provide a human readable display value
return value;
return toCurrencyValue(info.supply);
}
async mint(amount: Amount) {
@@ -92,20 +92,28 @@ export class Token {
};
}
async balance(): Promise<bigint> {
return this.balanceOf(this.wallet.getSigner().publicKey.toBase58());
async balance(): Promise<CurrencyValue> {
return this.balanceOf(this.metaplex.identity().publicKey.toBase58());
}
async balanceOf(walletAddress: string): Promise<bigint> {
async balanceOf(walletAddress: string): Promise<CurrencyValue> {
const mint = await this.getMint();
const addr = await getAssociatedTokenAddress(
this.tokenMintAddress,
new PublicKey(walletAddress),
);
try {
const account = await getAccount(this.connection, addr);
return account.amount;
return toCurrencyValue({
basisPoints: toBigNumber(account.amount.toString()),
currency: {
symbol: "",
decimals: mint.decimals,
namespace: "spl-token",
},
});
} catch (e) {
return 0n;
throw Error(`No balance found for address '${walletAddress}' - ${e}`);
}
}
}
+7
View File
@@ -6,3 +6,10 @@ export * from "./classes/user-wallet";
export * from "./contracts/nft-collection";
// sdk
export * from "./sdk";
//schema for dashboard use
export {
TokenMetadataInputSchema,
NFTCollectionMetadataInputSchema,
} from "./types/contracts";
export { NFTDropContractSchema } from "./types/contracts/nft-drop";
+22 -36
View File
@@ -1,4 +1,5 @@
import { Deployer } from "./classes/deployer";
import { Registry } from "./classes/registry";
import { UserWallet } from "./classes/user-wallet";
import { DEFAULT_IPFS_GATEWAY } from "./constants/urls";
import { NFTCollection } from "./contracts/nft-collection";
@@ -7,22 +8,15 @@ import { Program } from "./contracts/program";
import { Token } from "./contracts/token";
import { Network } from "./types";
import { getUrlForNetwork } from "./utils/urls";
import {
isIdentitySigner,
isKeypairSigner,
keypairIdentity,
Metaplex,
Signer,
walletAdapterIdentity,
} from "@metaplex-foundation/js";
import { Metaplex } from "@metaplex-foundation/js";
import {
AnchorProvider,
Idl,
Program as AnchorProgram,
setProvider,
} from "@project-serum/anchor";
import { Connection, Keypair } from "@solana/web3.js";
import { Connection } from "@solana/web3.js";
import { IpfsStorage, IStorage, PinataUploader } from "@thirdweb-dev/storage";
import invariant from "tiny-invariant";
export class ThirdwebSDK {
static fromNetwork(network: Network, storage?: IStorage): ThirdwebSDK {
@@ -34,6 +28,7 @@ export class ThirdwebSDK {
private anchorProvider: AnchorProvider;
private storage: IStorage;
public registry: Registry;
public deployer: Deployer;
public wallet: UserWallet;
@@ -50,34 +45,32 @@ export class ThirdwebSDK {
this.connection = connection;
this.storage = storage;
this.metaplex = Metaplex.make(this.connection);
this.wallet = new UserWallet();
this.deployer = new Deployer(this.metaplex, this.wallet, this.storage);
this.wallet = new UserWallet(this.metaplex);
this.deployer = new Deployer(this.metaplex, this.storage);
this.registry = new Registry(this.metaplex);
this.anchorProvider = new AnchorProvider(
this.metaplex.connection,
this.metaplex.identity(),
{},
);
// when there is a new signer connected in the wallet sdk, update that signer
this.wallet.events.on("connected", (s) => {
this.propagateSignerUpdated(s);
this.wallet.events.on("connected", () => {
this.propagateWalletConnected();
});
// when the wallet disconnects, update the signer to undefined
this.wallet.events.on("disconnected", () => {
// reset connection to Metaplex
this.metaplex = Metaplex.make(this.connection);
this.propagateWalletDisconnected();
});
}
public async getNFTCollection(address: string): Promise<NFTCollection> {
return new NFTCollection(address, this.metaplex, this.wallet, this.storage);
return new NFTCollection(address, this.metaplex, this.storage);
}
public async getNFTDrop(address: string): Promise<NFTDrop> {
return new NFTDrop(address, this.metaplex, this.wallet, this.storage);
return new NFTDrop(address, this.metaplex, this.storage);
}
public async getToken(address: string): Promise<Token> {
return new Token(address, this.metaplex, this.wallet, this.storage);
return new Token(address, this.metaplex, this.storage);
}
public async getProgram(address: string) {
@@ -94,23 +87,16 @@ export class ThirdwebSDK {
return new Program(address, idl, this.anchorProvider);
}
private propagateSignerUpdated(signer: Signer) {
this.metaplex = this.connectToMetaplex(signer, this.metaplex);
this.anchorProvider = new AnchorProvider(
this.metaplex.connection,
this.metaplex.identity(),
{},
private propagateWalletConnected() {
setProvider(
new AnchorProvider(this.connection, this.metaplex.identity(), {}),
);
}
private connectToMetaplex(signer: Signer, metaplex: Metaplex) {
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");
return metaplex.use(plugin);
private propagateWalletDisconnected() {
// metaplex.identity() will return a guest identity
setProvider(
new AnchorProvider(this.connection, this.metaplex.identity(), {}),
);
}
}
+10 -1
View File
@@ -1,3 +1,4 @@
import { Signer, WalletAdapter } from "@metaplex-foundation/js";
import { z } from "zod";
export const MAX_BPS = 10_000;
@@ -57,7 +58,7 @@ export const AmountSchema = z
export type Amount = z.input<typeof AmountSchema>;
export const CurrencyValueSchema = z.object({
value: z.bigint(),
value: z.string(),
displayValue: z.string(),
});
@@ -66,3 +67,11 @@ export type CurrencyValue = z.input<typeof CurrencyValueSchema>;
export type TransactionResult = {
signature: string;
};
export type WalletSigner = Signer | WalletAdapter;
export type AccountType = "nft-collection" | "nft-drop" | "token";
export type WalletAccount = {
type: AccountType;
address: string;
name: string;
};
+2 -1
View File
@@ -25,10 +25,11 @@ export const CommonContractOutputSchema = CommonContractSchema.extend({
export const NFTCollectionCreatorInputSchema = z.object({
address: z.string(),
share: z.number(),
verified: z.boolean().default(false),
});
export const NFTCollectionMetadataInputSchema = CommonContractSchema.extend({
creators: z.array(NFTCollectionCreatorInputSchema).optional(),
creators: z.array(NFTCollectionCreatorInputSchema).default([]),
});
export type NFTCollectionMetadataInput = z.input<
@@ -7,7 +7,7 @@ import { z } from "zod";
* @internal
*/
// TODO: Handle allow lists and end times
export const NFTDropContractSchema = z.object({
export const NFTDropConditionsSchema = z.object({
price: z
.number()
.default(0)
@@ -35,12 +35,16 @@ export const NFTDropContractSchema = z.object({
.optional(),
});
export const NFTDropContractSchema = NFTCollectionMetadataInputSchema.merge(
NFTDropConditionsSchema,
);
export type NFTDropMetadataInput = z.input<typeof NFTDropContractSchema>;
/**
* @internal
*/
export const NFTDropClaimSchema = NFTDropContractSchema.extend({
export const NFTDropClaimSchema = NFTDropConditionsSchema.extend({
price: z
.number()
.transform((p) => sol(p))
@@ -55,7 +59,7 @@ export const NFTDropClaimSchema = NFTDropContractSchema.extend({
/**
* @internal
*/
export const NFTDropOutputSchema = z.object({
export const NFTDropClaimOutputSchema = z.object({
price: z.bigint(),
sellerFeeBasisPoints: z.bigint(),
itemsAvailable: z.bigint(),
@@ -64,4 +68,4 @@ export const NFTDropOutputSchema = z.object({
export type NFTDropClaimInput = z.input<typeof NFTDropClaimSchema>;
export type NFTDropClaimOutput = z.output<typeof NFTDropOutputSchema>;
export type NFTDropClaimOutput = z.output<typeof NFTDropClaimOutputSchema>;
+8 -1
View File
@@ -46,10 +46,17 @@ export const CommonNFTOutput = CommonTokenOutput.extend({
animation_url: z.string().nullable().optional(),
});
/**
* @internal
*/
export const CommonFungibleTokenOutput = CommonTokenOutput.extend({
decimals: z.number(),
});
/**
* @public
*/
export type TokenMetadata = z.output<typeof CommonTokenOutput>;
export type TokenMetadata = z.output<typeof CommonFungibleTokenOutput>;
/**
* @public
*/
+28
View File
@@ -0,0 +1,28 @@
import { CurrencyValue } from "../types/common";
import { Amount } from "@metaplex-foundation/js";
import BN from "bn.js";
export function toCurrencyValue(amount: Amount): CurrencyValue {
return {
value: amount.basisPoints.toString(),
displayValue: toDisplayValue(amount),
};
}
const toDisplayValue = (value: Amount): string => {
if (value.currency.decimals === 0) {
return `${value.currency.symbol} ${value.basisPoints.toString()}`;
}
const power = new BN(10).pow(new BN(value.currency.decimals));
const basisPoints = value.basisPoints as unknown as BN & {
divmod: (other: BN) => { div: BN; mod: BN };
};
const { div, mod } = basisPoints.divmod(power);
const units = `${div.toString()}.${mod
.abs()
.toString(10, value.currency.decimals)}`;
return `${units}`;
};
+1 -1
View File
@@ -8,7 +8,7 @@ import { Amman } from "@metaplex-foundation/amman-client";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { Connection, Keypair } from "@solana/web3.js";
const createTestSDK = async (
export const createTestSDK = async (
solsToAirdrop: number = 100,
): Promise<ThirdwebSDK> => {
const connection = new Connection("http://localhost:8899", "confirmed");
+1
View File
@@ -8,6 +8,7 @@ describe("NFTDrop", async () => {
before(async () => {
const address = await sdk.deployer.createNftDrop({
name: "Test Drop",
price: 0,
sellerFeeBasisPoints: 0,
itemsAvailable: 5,
+56
View File
@@ -0,0 +1,56 @@
import { ThirdwebSDK } from "../src/index";
import { createTestSDK } from "./before-setup";
import { expect } from "chai";
describe("Registry", async () => {
let nftColl: string;
let nftDrop: string;
let token: string;
let freshSDK: ThirdwebSDK;
before(async () => {
freshSDK = await createTestSDK();
nftColl = await freshSDK.deployer.createNftCollection({
name: "Reg Test Collection",
description: "Test Description",
symbol: "TC",
});
token = await freshSDK.deployer.createToken({
name: "Reg Test Token",
initialSupply: 100,
});
nftDrop = await freshSDK.deployer.createNftDrop({
name: "Reg Test Drop",
});
});
it("should fetch accounts grouped by type", async () => {
const t = await freshSDK.registry.getAccountsForWallet(
freshSDK.wallet.getAddress() || "",
);
t.forEach((account) => {
switch (account.type) {
case "nft-collection":
expect(account.name).to.equal("Reg Test Collection");
expect(account.address).to.equal(nftColl);
break;
case "nft-drop":
expect(account.name).to.equal("Reg Test Drop");
expect(account.address).to.equal(nftDrop);
break;
case "token":
expect(account.name).to.equal("Reg Test Token");
expect(account.address).to.equal(token);
break;
}
});
});
it("should resolve account addresses", async () => {
expect(await freshSDK.registry.getAccountType(nftColl)).to.eq(
"nft-collection",
);
expect(await freshSDK.registry.getAccountType(nftDrop)).to.eq("nft-drop");
expect(await freshSDK.registry.getAccountType(token)).to.eq("token");
});
});
+5 -4
View File
@@ -9,6 +9,7 @@ describe("Token", async () => {
before(async () => {
const tokenAddr = await sdk.deployer.createToken({
name: "My Token",
symbol: "MTK",
initialSupply: 100,
});
token = await sdk.getToken(tokenAddr);
@@ -20,15 +21,15 @@ describe("Token", async () => {
});
it("mint more should update total supply", async () => {
expect((await token.totalSupply()).toString()).to.eq("100000000000");
expect((await token.totalSupply()).displayValue).to.eq("100.000000000");
await token.mint(3);
expect((await token.totalSupply()).toString()).to.eq("103000000000");
expect((await token.totalSupply()).displayValue).to.eq("103.000000000");
});
it("transfer should update total supply", async () => {
expect((await token.balance()).toString()).to.eq("103000000000");
expect((await token.balance()).displayValue).to.eq("103.000000000");
const receiver = Keypair.generate();
await token.transfer(receiver.publicKey.toBase58(), 10);
expect((await token.balance()).toString()).to.eq("93000000000");
expect((await token.balance()).displayValue).to.eq("93.000000000");
});
});