diff --git a/test.png b/test.png new file mode 100644 index 00000000..fe3975fe Binary files /dev/null and b/test.png differ diff --git a/tests/test_constants.py b/tests/constants/__init__.py similarity index 67% rename from tests/test_constants.py rename to tests/constants/__init__.py index 545c1ce7..a3f4cd69 100644 --- a/tests/test_constants.py +++ b/tests/constants/__init__.py @@ -1,7 +1,7 @@ -TEST_NFT_CONTRACT_ADDRESS = "0xEeD541b524Ae738c48211Be91EB81E97739A0A29" +TEST_NFT_CONTRACT_ADDRESS = "0xedd431613Ae5EF32D006F2Ad1fC229d939f86C64" TEST_PACK_CONTRACT_ADDRESS = "0x54ec360704b2e9E4e6499a732b78094D6d78e37B" TEST_MARKET_CONTRACT_ADDRESS = "0x325a98B6081ef88C6356d63c56f48Fa1d0d2DD0D" -TEST_BUNDLE_CONTRACT_ADDRESS = "0x6Da734b14e4CE604f1e18efb7E7f7ef022e96616" +TEST_BUNDLE_CONTRACT_ADDRESS = "0xc5392102B97496413fCCe4c823bF1F674856B382" TEST_CURRENCY_CONTRACT_ADDRESS = "0xF18FEb8b2F58691d67C98dB98B360840df340e74" TEST_COMPANION_WALLET_ADDRESS = "0x4d36d531D9cB40b8694763123D52170FAE5e1195" diff --git a/tests/test.png b/tests/test.png new file mode 100644 index 00000000..ad468a76 Binary files /dev/null and b/tests/test.png differ diff --git a/tests/test_bundle.py b/tests/test_bundle.py index 9f068925..83972d26 100644 --- a/tests/test_bundle.py +++ b/tests/test_bundle.py @@ -1,12 +1,13 @@ import unittest from os import environ -from dataclasses_json.api import A - from thirdweb import SdkOptions, ThirdwebSdk from thirdweb.modules.bundle import BundleModule from thirdweb.modules.collection import CollectionModule -from thirdweb.types.collection import CreateCollectionArg + +from .constants import (TEST_BUNDLE_CONTRACT_ADDRESS, + TEST_CURRENCY_CONTRACT_ADDRESS, + TEST_NFT_CONTRACT_ADDRESS) class TestRoles(unittest.TestCase): @@ -19,10 +20,9 @@ def setUpClass(self): self.sdk = ThirdwebSdk(SdkOptions( private_key=environ['PKEY'] ), "https://rpc-mumbai.maticvigil.com") - self.module = self.sdk.get_bundle_module( - "0x5CF412451f4Cef34293604048238bd18D2BD1e71") - self.old_module = self.sdk.get_collection_module( - "0x5CF412451f4Cef34293604048238bd18D2BD1e71") + contract_address = TEST_BUNDLE_CONTRACT_ADDRESS + self.module = self.sdk.get_bundle_module(contract_address) + self.old_module = self.sdk.get_collection_module(contract_address) def test_bundle_get_all(self): """ @@ -40,19 +40,25 @@ def test_collection_get_all(self): self.assertGreater( len(result), 0, "There should be at least 1 token in the contract") - # def test_collection_mint(self): + def test_bundle_create(self): + """ + Test that tries to instantiate the Bundle module + """ + result = self.module.create({"name": "test"}) + self.assertIsNotNone(result, "The result should not be None") + + def test_bundle_create_with_token(self): + """ + Test that tries to instantiate the Bundle module + """ + result = self.module.create_with_token( + TEST_CURRENCY_CONTRACT_ADDRESS, 20, {}) + + # def test_bundle_create_with_nft(self): # """ - # Test that tries to instantiate the Collection module + # Test that tries to instantiate the Bundle module # """ - # result = self.old_module.create_and_mint(meta_with_supply=CreateCollectionArg( - # metadata={"name": "Test"}, - # supply=10, - # )) - # print("Minted", result) - # result = self.module.create_and_mint(meta_with_supply=CreateCollectionArg( - # metadata={"name": "Test"}, - # supply=10, - # )) + # result = self.module.create_with_nft(TEST_NFT_CONTRACT_ADDRESS, 1, {}) if __name__ == '__main__': diff --git a/tests/test_import.py b/tests/test_import.py index e6803d35..62758445 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -15,37 +15,41 @@ def test_init_nft_module(self): Test that tries to instantiate the NFT module """ sdk = NftlabsSdk(SdkOptions(), "https://rpc-mumbai.maticvigil.com") - nft_module = sdk.get_nft_module("0xEeD541b524Ae738c48211Be91EB81E97739A0A29") + nft_module = sdk.get_nft_module( + "0xEeD541b524Ae738c48211Be91EB81E97739A0A29") def test_init_currency_module(self): """ Test that tries to instantiate the Currency module """ sdk = NftlabsSdk(SdkOptions(), "https://rpc-mumbai.maticvigil.com") - currency_module = sdk.get_currency_module("0xF18FEb8b2F58691d67C98dB98B360840df340e74") + currency_module = sdk.get_currency_module( + "0xF18FEb8b2F58691d67C98dB98B360840df340e74") - def test_init_bundle_module(self): """ Test that tries to instantiate the Bundle module """ sdk = NftlabsSdk(SdkOptions(), "https://rpc-mumbai.maticvigil.com") - bundle_module = sdk.get_bundle_module("0x6Da734b14e4CE604f1e18efb7E7f7ef022e96616") + bundle_module = sdk.get_bundle_module( + "0x6Da734b14e4CE604f1e18efb7E7f7ef022e96616") def test_init_pack_module(self): """ Test that tries to instantiate the Pack module """ sdk = NftlabsSdk(SdkOptions(), "https://rpc-mumbai.maticvigil.com") - pack_module = sdk.get_pack_module("0x54ec360704b2e9E4e6499a732b78094D6d78e37B") + pack_module = sdk.get_pack_module( + "0x54ec360704b2e9E4e6499a732b78094D6d78e37B") def test_init_marketplace_module(self): """ Test that tries to instantiate the Marketplace module """ sdk = NftlabsSdk(SdkOptions(), "https://rpc-mumbai.maticvigil.com") - pack_module = sdk.get_market_module("0xD3920A1fd0fB09EA00F8ce56d0c655CF7a50428C") + pack_module = sdk.get_market_module( + "0xD3920A1fd0fB09EA00F8ce56d0c655CF7a50428C") if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/test_market.py b/tests/test_market.py index 4a5936d9..6915eee7 100644 --- a/tests/test_market.py +++ b/tests/test_market.py @@ -1,10 +1,10 @@ import unittest -from nftlabs import NftlabsSdk, SdkOptions +from thirdweb import ThirdwebSdk, SdkOptions -from test_constants import (TEST_BUNDLE_CONTRACT_ADDRESS, - TEST_MARKET_CONTRACT_ADDRESS, - TEST_NFT_CONTRACT_ADDRESS) +from .constants import (TEST_BUNDLE_CONTRACT_ADDRESS, + TEST_MARKET_CONTRACT_ADDRESS, + TEST_NFT_CONTRACT_ADDRESS) class TestMarket(unittest.TestCase): @@ -12,7 +12,7 @@ def test_init_marketplace_module(self): """ Test that tries to instantiate the Marketplace module """ - sdk = NftlabsSdk(SdkOptions(), "https://rpc-mumbai.maticvigil.com") + sdk = ThirdwebSdk(SdkOptions(), "https://rpc-mumbai.maticvigil.com") market_module = sdk.get_market_module(TEST_MARKET_CONTRACT_ADDRESS) self.assertFalse(market_module.is_erc721( diff --git a/tests/test_roles.py b/tests/test_roles.py index 5a5b3893..30a9e6fc 100644 --- a/tests/test_roles.py +++ b/tests/test_roles.py @@ -1,9 +1,14 @@ import unittest - -from nftlabs import NftlabsSdk, SdkOptions -from nftlabs.types.role import Role from os import environ -from test_constants import TEST_BUNDLE_CONTRACT_ADDRESS, TEST_CURRENCY_CONTRACT_ADDRESS, TEST_MARKET_CONTRACT_ADDRESS, TEST_NFT_CONTRACT_ADDRESS, TEST_COMPANION_WALLET_ADDRESS, TEST_PACK_CONTRACT_ADDRESS + +from thirdweb import SdkOptions, ThirdwebSdk +from thirdweb.types.role import Role + +from .constants import (TEST_BUNDLE_CONTRACT_ADDRESS, + TEST_COMPANION_WALLET_ADDRESS, + TEST_CURRENCY_CONTRACT_ADDRESS, + TEST_MARKET_CONTRACT_ADDRESS, + TEST_NFT_CONTRACT_ADDRESS, TEST_PACK_CONTRACT_ADDRESS) class TestRoles(unittest.TestCase): @@ -11,7 +16,7 @@ def test_grant_and_revoke_role(self): """ Test that tries to instantiate the NFT module """ - sdk = NftlabsSdk(SdkOptions( + sdk = ThirdwebSdk(SdkOptions( private_key=environ['PKEY'] ), "https://rpc-mumbai.maticvigil.com") diff --git a/tests/test_storage.py b/tests/test_storage.py new file mode 100644 index 00000000..f07e8e2e --- /dev/null +++ b/tests/test_storage.py @@ -0,0 +1,33 @@ +import os +import unittest + +from thirdweb import MintArg, ThirdwebSdk, SdkOptions + + +class TestStorage(unittest.TestCase): + def test_storage(self): + sdk = ThirdwebSdk(SdkOptions( + private_key=os.environ['PKEY'] + ), "https://rpc-mumbai.maticvigil.com") + + nft_module = sdk.get_nft_module( + "0xEeD541b524Ae738c48211Be91EB81E97739A0A29") + + # mint by uploading a file + with open(file='tests/test.png', mode='rb') as f: + content = f.read() + module = nft_module.mint(MintArg(name="example", + description="example nft!", image=content, properties={})) + self.assertTrue(module.image.startswith( + "ipfs://"), "Image files are expected to be uploaded when set in the image property") + + # mint by providing a string + actual_image = "ipfs://QmYWi4mkEjsL6MYoS8z2ZWPAhyDPNjPQ2pqg8MGEM1CaeQ" + module = nft_module.mint(MintArg(name="example", description="example nft!", + image=actual_image, properties={})) + self.assertEqual(module.image, actual_image, + "String image properties should be uploaded as-is") + + +if __name__ == '__main__': + unittest.main() diff --git a/thirdweb/errors/__init__.py b/thirdweb/errors/__init__.py index a875f002..b1bf185e 100644 --- a/thirdweb/errors/__init__.py +++ b/thirdweb/errors/__init__.py @@ -1,5 +1,5 @@ from typing import Any, Optional - +import json class NoSignerException(Exception): def __init__(self): @@ -16,3 +16,8 @@ class UnsupportedAssetException(Exception): def __init__(self, identifier: Optional[Any] = None): super().__init__( f"Asset with address {identifier} is not compatible with this method") + +class UploadError(Exception): + def __init__(self, message: str): + super().__init__(f"There was an error while uploading the image : {message}") + diff --git a/thirdweb/modules/base.py b/thirdweb/modules/base.py index 0ec00367..992658d6 100644 --- a/thirdweb/modules/base.py +++ b/thirdweb/modules/base.py @@ -7,7 +7,7 @@ from thirdweb_web3 import Web3 from thirdweb_web3.types import TxReceipt from zero_ex.contract_wrappers import TxParams - +import json from ..abi.coin import Coin from ..abi.erc165 import ERC165 from ..abi.market import Market @@ -16,6 +16,7 @@ from ..abi.pack import Pack from ..constants.erc_interfaces import InterfaceIdErc721, InterfaceIdErc1155 from ..errors import NoSignerException +import io from ..options import SdkOptions from ..storage import IpfsStorage from ..types.role import Role @@ -52,7 +53,7 @@ def __init__(self): self.get_options = None def execute_tx(self, tx) -> TxReceipt: - """ + """ Execute a transaction and return the receipt. """ client = self.get_client() @@ -68,7 +69,7 @@ def execute_tx(self, tx) -> TxReceipt: ) def __sign_tx(self, tx): - """ + """ Sign a transaction. """ signed_tx = self.get_account().sign_transaction(tx) @@ -102,6 +103,24 @@ def grant_role(self, role: Role, address: str): ) self.execute_tx(tx) + def upload_metadata(self, data: Union[Dict, str]) -> str: + """ + Uploads the metadata to IPFS and returns the uri. + """ + storage = self.get_storage() + if isinstance(data, str) and data.startswith("ipfs://"): + return data + + if 'image_uri' in data and data["image"] == "": + data["image"] = data["image_uri"] + + if 'image' in data: + if isinstance(data["image"], bytes) or isinstance(data["image"], bytearray): + data["image"] = storage.upload( + data["image"], self.address, self.get_signer_address()) + + return storage.upload(json.dumps(data), self.address, self.get_signer_address()) + def revoke_role(self, role: Role, address: str): """ Revokes the given role from the given address diff --git a/thirdweb/modules/bundle.py b/thirdweb/modules/bundle.py index eccb62a7..0711b954 100644 --- a/thirdweb/modules/bundle.py +++ b/thirdweb/modules/bundle.py @@ -5,11 +5,14 @@ from ..abi.nft_collection import NFTCollection as NFTBundle # from ..types.collection import (BundleMetadata, CreateBundleArg, # MintBundleArg) +from ..abi.erc20 import ERC20 +from ..constants import ZeroAddress + from ..types.bundle import (BundleMetadata, CreateBundleArg, MintBundleArg) from ..types.metadata import Metadata from ..types.nft import NftMetadata from .base import BaseModule - +from ..abi.nft import NFT class BundleModule(BaseModule): address: str @@ -52,11 +55,13 @@ def balance(self, token_id: int) -> int: token_id ) - def is_approved(self, address: str, operator: str) -> bool: - return self.__abi_module.is_approved_for_all.call( - address, - operator - ) + def is_approved(self, address: str, operator: str, token_contract: str = None, token_id: int = None) -> bool: + if not token_contract: return self.__abi_module.is_approved_for_all.call(address,operator) + asset = NFT(self.get_client(), token_contract) + approved = asset.is_approved_for_all.call(address, operator) + is_token_approved = asset.get_approved.call(token_id).lower() == self.address.lower() + return approved or is_token_approved + def set_approval(self, operator: str, approved: bool = True): self.execute_tx(self.__abi_module.set_approval_for_all.build_transaction( @@ -80,8 +85,8 @@ def create_and_mint(self, meta_with_supply: CreateBundleArg) -> BundleMetadata: return self.create_and_mint_batch([meta_with_supply])[0] def create_and_mint_batch(self, meta_with_supply: List[CreateBundleArg]) -> List[BundleMetadata]: - uris = [self.get_storage().upload(meta.to_json(), self.address, - self.get_signer_address()) for meta in meta_with_supply] + uris = [self.upload_metadata(meta.metadata) + for meta in meta_with_supply] supplies = [a.supply for a in meta_with_supply] receipt = self.execute_tx(self.__abi_module.create_native_tokens.build_transaction( self.get_signer_address(), uris, supplies, "", self.get_transact_opts() @@ -91,20 +96,50 @@ def create_and_mint_batch(self, meta_with_supply: List[CreateBundleArg]) -> List token_ids = result[0]['args']['tokenIds'] return [self.get(i) for i in token_ids] - def create_with_erc20(self, token_contract: str, token_amount: int, arg: CreateBundleArg): - uri = self.get_storage().upload( - arg.metadata, self.address, self.get_signer_address()) + def create_with_token(self, token_contract: str, token_amount: int, metadata: dict = None): + uri = self.upload_metadata(metadata) + if token_contract is not None and token_contract != ZeroAddress: + erc20 = ERC20(self.get_client(), token_contract) + allowance = erc20.allowance.call(self.get_signer_address(), self.address) + if allowance < token_amount: + tx = erc20.increase_allowance.build_transaction(self.address, + token_amount, + self.get_transact_opts()) + self.execute_tx(tx) + self.execute_tx(self.__abi_module.wrap_erc20.build_transaction( - token_contract, token_amount, arg.supply, uri, self.get_transact_opts() + token_contract, token_amount, token_amount, uri, self.get_transact_opts() )) - - def create_with_erc721(self, token_contract: str, token_id: int, metadata): - uri = self.get_storage().upload( - metadata.metadata, self.address, self.get_signer_address()) + + def create_with_nft(self, token_contract: str, token_id: int, metadata): + """ + WIP: This method is not yet complete. + """ + #token_module = sdk.get_nft_module(token_contract) + nft_module = NFT(self.get_client(), token_contract) + + asset = NFT(self.get_client(), token_contract) + approved = asset.is_approved_for_all.call(self.get_signer_address(), self.address) + + + if not approved: + is_token_approved = asset.get_approved.call( + token_id).lower() == self.address.lower() + if not is_token_approved: + self.execute_tx(asset.set_approval_for_all.build_transaction( + self.address, True, self.get_transact_opts())) + + uri = self.upload_metadata(metadata) self.execute_tx(self.__abi_module.wrap_erc721.build_transaction( token_contract, token_id, uri, self.get_transact_opts() )) + def create_with_erc721(self, token_contract: str, token_id: int, metadata): + return create_with_nft(token_contract, token_id, metadata) + + def create_with_erc20(self, token_contract: str, token_amount: int, metadata): + return create_with_token(token_contract, token_amount, metadata) + def mint(self, args: MintBundleArg): self.mint_to(self.get_signer_address(), args) diff --git a/thirdweb/modules/nft.py b/thirdweb/modules/nft.py index 09c0157f..678e99ac 100644 --- a/thirdweb/modules/nft.py +++ b/thirdweb/modules/nft.py @@ -2,6 +2,7 @@ import copy import json from typing import Dict, List +import io from thirdweb_web3 import Web3 @@ -19,7 +20,6 @@ class NftModule(BaseModule): address: str __abi_module: NFT - def __init__(self, address: str, client: Web3): """ Initializing the class attributes @@ -42,7 +42,6 @@ def mint(self, arg: MintArg) -> NftType: - Returns the `NftMetadata(name,description,image,properties,id,uri)` *Preferrably, using a link """ return self.mint_to(self.get_signer_address(), arg) - def mint_to( self, @@ -59,16 +58,18 @@ def mint_to( final_properties = {} else: final_properties = copy.copy(arg.properties) - storage = self.get_storage() + + if arg.image == "": + arg.image = arg.image_uri meta = { - "name": arg.name, - "description": arg.description, - "image": arg.image_uri, - "properties": final_properties, + 'name': arg.name, + 'description': arg.description, + 'image': arg.image, + 'properties': final_properties } - uri = storage.upload(json.dumps(meta), self.address, self.get_signer_address()) + uri = self.upload_metadata(meta) tx = self.__abi_module.mint_nft.build_transaction( to_address, uri, self.get_transact_opts() ) @@ -102,7 +103,8 @@ def __get_metadata_uri(self, token_id: int): """ uri = self.__abi_module.token_uri.call(token_id) if uri == "": - raise Exception("Could not find NFT metadata, are you sure it exists?") + raise Exception( + "Could not find NFT metadata, are you sure it exists?") return uri def mint_batch(self, args: List[MintArg]): @@ -115,23 +117,12 @@ def mint_batch_to(self, to_address: str, args: List[MintArg]): """ Mints a batch of tokens to the given address """ - uris = [ - self.get_storage().upload( - json.dumps( - { - "name": arg.name, - "description": arg.description, - "image": arg.image_uri, - "properties": arg.properties - if arg.properties is not None - else {}, - } - ), - self.address, - self.get_signer_address(), - ) - for arg in args - ] + uris = [self.upload_metadata({ + 'name': arg.name, + 'description': arg.description, + 'image': arg.image, + 'properties': arg.properties if arg.properties is not None else {} + }) for arg in args] tx = self.__abi_module.mint_nft_batch.build_transaction( to_address, uris, self.get_transact_opts() @@ -205,7 +196,6 @@ def get_owned(self, address: str = "") -> List[NftType]: def __token_of_owner_by_index(self, address: str, token_id: int) -> int: return self.__abi_module.token_of_owner_by_index.call(address, token_id) - def balance(self) -> int: """ Returns balance of the current signers wallet @@ -214,9 +204,8 @@ def balance(self) -> int: - Dashboard: Project ➝ NFT Module ➝ Total amount of NFT's """ - + return self.__abi_module.balance_of.call(self.get_signer_address()) - def balance_of(self, address: str) -> int: """ diff --git a/thirdweb/modules/nft_types.py b/thirdweb/modules/nft_types.py index c8b1932a..2c0cd0b3 100644 --- a/thirdweb/modules/nft_types.py +++ b/thirdweb/modules/nft_types.py @@ -1,6 +1,7 @@ """Types for the NFT Module.""" from dataclasses import dataclass -from typing import Optional +from typing import Optional, Union +import io @dataclass @@ -10,6 +11,7 @@ class MintArg: """ name: str description: str = "" - image_uri: str = "" + image: Union[str, io.TextIOWrapper] = "" properties: Optional[dict] = None + image_uri: str = "" diff --git a/thirdweb/modules/pack.py b/thirdweb/modules/pack.py index 3fd79f4e..db22864f 100644 --- a/thirdweb/modules/pack.py +++ b/thirdweb/modules/pack.py @@ -85,8 +85,7 @@ def create(self, arg: CreatePackArg) -> PackMetadata: from_address = self.get_signer_address() ids = [a.token_id for a in arg.assets] amounts = [a.amount for a in arg.assets] - uri = self.get_storage().upload( - dumps(arg.metadata), self.address, self.get_signer_address()) + uri = self.upload_metadata(arg.metadata) params = encode_abi( ['string', 'uint256', 'uint256'], diff --git a/thirdweb/storage/ipfs_storage.py b/thirdweb/storage/ipfs_storage.py index 1ec1812f..69fc4344 100644 --- a/thirdweb/storage/ipfs_storage.py +++ b/thirdweb/storage/ipfs_storage.py @@ -2,6 +2,10 @@ from .util import replace_ipfs_prefix_with_gateway from requests import get, post import json +import io +from ..errors import UploadError + +import base64 class IpfsStorage: @@ -29,17 +33,15 @@ def get(self, uri: str) -> str: def upload(self, data, contract_address: str, signer_address: str) -> str: if isinstance(data, str) and data.startswith("ipfs://"): return data - form = { - 'file': (None, data) + 'file': data } result = post(f'{self.__nftlabsApiUrl}/upload', files=form, headers={ - 'X-App-Name': f'CONSOLE-GO-SDK-{contract_address}', + 'X-App-Name': f'CONSOLE-PYTHON-SDK-{contract_address}', 'X-Public-Address': signer_address, }) if result.status_code != 200: - raise Exception("Failed to upload metadata") - + raise UploadError(result.text) response = result.json() return response['IpfsUri'] diff --git a/thirdweb/types/nft/__init__.py b/thirdweb/types/nft/__init__.py index d8f3faeb..391cf8a1 100644 --- a/thirdweb/types/nft/__init__.py +++ b/thirdweb/types/nft/__init__.py @@ -1,15 +1,15 @@ from dataclasses import dataclass from typing import Optional, Union - from dataclasses_json import dataclass_json - +import io @dataclass class MintArg: name: str description: str = "" - image_uri: str = "" + image: Union[str, io.TextIOWrapper] = "" properties: Optional[dict] = None + image_uri: str = "" @dataclass_json