4 |
5 | This is the code repository for [Hands-On Blockchain for Python Developers](https://www.packtpub.com/en-us/product/hands-on-blockchain-for-python-developers-9781805121367?utm_source=github&utm_medium=repository&utm_campaign=), published by Packt.
6 |
7 | **Empowering Python developers in the world of blockchain and smart contracts**
8 |
9 | ## What is this book about?
10 | We are living in the age of decentralized fi nance and NFTs. People swap tokens on Uniswap, borrow assets from Aave, send payments with stablecoins, trade art NFTs on OpenSea, and more. To build applications of this kind, you need to know how to write smart contracts.
11 |
12 | This book covers the following exciting features:
13 | * Understand blockchain and smart contracts
14 | * Learn how to write smart contracts with Vyper
15 | * Explore how to use the web3.py library and Ape Framework
16 | * Discover related technologies such as Layer 2 and IPFS
17 | * Gain a step-by-step guide to writing an automated market maker (AMM) decentralized exchange (DEX) smart contract
18 | * Build innovative, interactive, and token-gated Web3 NFT applications
19 |
20 | If you feel this book is for you, get your [copy](https://www.amazon.com/dp/1805121367) today!
21 |
22 |
24 |
25 | ## Instructions and Navigations
26 | All of the code is organized into folders.
27 |
28 | The code will look like the following:
29 | ```
30 | from ape import accounts, project
31 | import os
32 | def main():
33 | password = os.environ["MY_PASSWORD"]
34 | dev = accounts.load("dev")
35 | dev.set_autosign(True, passphrase=password)
36 | contract = project.SimpleStorage.deploy(sender=dev)
37 | num_value = contract.retrieve.call()
38 | print(f"The num value is {num_value}")
39 | ```
40 |
41 | **Following is what you need for this book:**
42 | This blockchain book is for developers interested in understanding blockchain and smart contracts. It is suitable for both technology enthusiasts looking to explore blockchain technology and programmers who aspire to become smart contract engineers. Basic knowledge of GNU/Linux and Python programming is mandatory to get started with this book.
43 |
44 | With the following software and hardware list you can run all code files present in the book (Chapter 1-17).
45 | ### Software and Hardware List
46 | | Chapter | Software required | OS required |
47 | | -------- | ------------------------------------ | ----------------------------------- |
48 | | 1-17 | Python (minimum version 3.10 required) | Windows, Mac OS X, and Linux (Any) |
49 | | 1-17 | Vyper 0.3.10 (versions 0.4.x and above will not work) | Windows, Mac OS X, and Linux (Any) |
50 | | 1-17 | Ape Framework 0.7.23 (versions 0.8.x and above will not work) | Windows, Mac OS X, and Linux (Any) |
51 | | 1-17 | Modern browsers (Mozilla, Firefox, Chrome, etc) | Windows, Mac OS X, and Linux (Any) |
52 | | 1-17 | Remix | Windows, Mac OS X, and Linux (Any) |
53 | | 1-17 | Ganache | Windows, Mac OS X, and Linux (Any) |
54 | | 1-17 | Hardhat | Windows, Mac OS X, and Linux (Any) |
55 | | 1-17 | Geth | Windows, Mac OS X, and Linux (Any) |
56 | | 1-17 | Web3.py | Windows, Mac OS X, and Linux (Any) |
57 | | 1-17 | PySide 6 | Windows, Mac OS X, and Linux (Any) |
58 | | 1-17 | Kubo | Windows, Mac OS X, and Linux (Any) |
59 | | 1-17 | aioipfs | Windows, Mac OS X, and Linux (Any) |
60 | | 1-17 | Alchemy | Windows, Mac OS X, and Linux (Any) |
61 | | 1-17 | Infura | Windows, Mac OS X, and Linux (Any) |
62 | | 1-17 | Django | Windows, Mac OS X, and Linux (Any) |
63 | | 1-17 | FastAPI | Windows, Mac OS X, and Linux (Any) |
64 | | 1-17 | Node.js | Windows, Mac OS X, and Linux (Any) |
65 | | 1-17 | Pnpm | Windows, Mac OS X, and Linux (Any) |
66 | | 1-17 | React | Windows, Mac OS X, and Linux (Any) |
67 | | 1-17 | Wagmi | Windows, Mac OS X, and Linux (Any) |
68 | | 1-17 | MetaMask | Windows, Mac OS X, and Linux (Any) |
69 |
70 | ### Related products
71 | * Solidity Programming Essentials [[Packt]](https://www.packtpub.com/en-ar/product/solidity-programming-essentials-9781803231181?utm_source=github&utm_medium=repository&utm_campaign=9781839216862) [[Amazon]](https://www.amazon.com/dp/1803231181)
72 |
73 | * Applied Computational Thinking with Python [[Packt]](https://www.packtpub.com/en-IT/product/applied-computational-thinking-with-python-9781837632305?utm_source=github&utm_medium=repository&utm_campaign=9781803239545) [[Amazon]](https://www.amazon.com/dp/1837632308)
74 |
75 | ## Get to Know the Author
76 | **Arjuna Sky Kok**
77 | is a skilled software engineer with a passion for all things related to finance and
78 | technology. He lives in Jakarta, where he studied mathematics and programming at Binus University.
79 | Arjuna's academic achievements include double degrees in Computer Science and Mathematics.
80 | Currently, he is focusing his talent in the crypto space, as he believes that DeFi and NFT will serve as
81 | the foundation for future fi nance. He also has a keen interest in AI, especially Generative AI. Outside
82 | of work, Arjuna enjoys watching anime, listening to J-pop songs, and playing basketball.
83 |
--------------------------------------------------------------------------------
/chapter_17/token-gated-backend/token_gated_app.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta, timezone
2 | from string import Template
3 | import string
4 | import json
5 | import secrets
6 | import os
7 | from typing import Union
8 |
9 | from ape import Contract
10 | from ape import networks
11 | from ethpm_types import ContractType
12 |
13 | from fastapi import Depends, FastAPI, HTTPException, status
14 | from fastapi.security import OAuth2PasswordBearer
15 | from fastapi.middleware.cors import CORSMiddleware
16 | import jwt
17 | from pydantic import BaseModel
18 | from typing_extensions import Annotated
19 | from siwe import SiweMessage
20 | import siwe
21 |
22 | # to get a string like this run:
23 | # openssl rand -hex 32
24 | SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
25 | ALGORITHM = "HS256"
26 | ACCESS_TOKEN_EXPIRE_MINUTES = 30
27 |
28 | EIP_4361_STRING = Template("""
29 | packtpub.com wants you to sign in with your Ethereum account:
30 | $address
31 |
32 | I accept the PacktPub Terms of Service: https://www.packtpub.com/en-us/help/terms-and-conditions
33 |
34 | URI: http://127.0.0.1:8000/token
35 | Version: 1
36 | Chain ID: 1
37 | Nonce: $nonce
38 | Issued At: $nonce_time
39 | Resources:
40 | - https://github.com/PacktPublishing/Hands-On-Blockchain-for-Python-Developers--2nd-Edition
41 | """)
42 |
43 | nonces_data = {}
44 |
45 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
46 |
47 |
48 | class Token(BaseModel):
49 | access_token: str
50 | token_type: str
51 |
52 | class Nonce(BaseModel):
53 | nonce: str
54 | nonce_time: str
55 |
56 | class Crypto(BaseModel):
57 | address: str
58 | signature: str
59 |
60 | class User(BaseModel):
61 | address: str
62 |
63 | def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None):
64 | to_encode = data.copy()
65 | if expires_delta:
66 | expire = datetime.now(timezone.utc) + expires_delta
67 | else:
68 | expire = datetime.now(timezone.utc) + timedelta(minutes=15)
69 | to_encode.update({"exp": expire})
70 | encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
71 | return encoded_jwt
72 |
73 | def generate_nonce(length=16):
74 | chars = string.ascii_letters + string.digits
75 | return ''.join(secrets.choice(chars) for _ in range(length))
76 |
77 |
78 | app = FastAPI()
79 |
80 | origins = [
81 | "http://localhost:5173",
82 | ]
83 |
84 | app.add_middleware(
85 | CORSMiddleware,
86 | allow_origins=origins,
87 | allow_credentials=True,
88 | allow_methods=["*"],
89 | allow_headers=["*"]
90 | )
91 |
92 |
93 | @app.get("/nonce/{address}")
94 | async def nonce(address: str) -> Nonce:
95 | nonce = generate_nonce()
96 | nonce_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
97 | nonces_data[address] = {"nonce": nonce, "nonce_time": nonce_time}
98 | return nonces_data[address]
99 |
100 | @app.post("/token")
101 | async def login_for_access_token(
102 | crypto: Crypto,
103 | ) -> Token:
104 | address = crypto.address
105 | nonce = nonces_data[address]["nonce"]
106 | nonce_time = nonces_data[address]["nonce_time"]
107 | signature = crypto.signature
108 | eip_string = EIP_4361_STRING.substitute(address=address, nonce=nonce, nonce_time=nonce_time)
109 | message = SiweMessage.from_message(message=eip_string, abnf=False)
110 | try:
111 | message.verify(signature=signature)
112 | access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
113 | access_token = create_access_token(
114 | data={"sub": address}, expires_delta=access_token_expires
115 | )
116 | return Token(access_token=access_token, token_type="bearer")
117 | except siwe.VerificationError:
118 | raise HTTPException(
119 | status_code=status.HTTP_401_UNAUTHORIZED,
120 | detail="Incorrect signature",
121 | headers={"WWW-Authenticate": "Bearer"},
122 | )
123 |
124 | async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
125 | credentials_exception = HTTPException(
126 | status_code=status.HTTP_401_UNAUTHORIZED,
127 | detail="Could not validate credentials",
128 | headers={"WWW-Authenticate": "Bearer"},
129 | )
130 | try:
131 | payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
132 | address: str = payload.get("sub")
133 | if address is None:
134 | raise credentials_exception
135 | except jwt.exceptions.InvalidSignatureError:
136 | raise credentials_exception
137 | exists = address in nonces_data
138 | if not exists:
139 | raise credentials_exception
140 | return User(address=address)
141 |
142 | async def get_current_active_user(
143 | current_user: Annotated[User, Depends(get_current_user)],
144 | ):
145 | return current_user
146 |
147 | @app.get("/me", response_model=User)
148 | async def read_users_me(
149 | current_user: Annotated[User, Depends(get_current_active_user)],
150 | ):
151 | return current_user
152 |
153 | BLOCKCHAIN_NETWORK = "local"
154 | BLOCKCHAIN_PROVIDER = "geth"
155 |
156 | @app.get("/content")
157 | async def read_content(
158 | current_user: Annotated[User, Depends(get_current_active_user)],
159 | ):
160 | with open('../token-gated-smart-contract/.build/HelloNFT.json') as f:
161 | contract = json.load(f)
162 | abi = contract['abi']
163 |
164 | nft_address = os.environ["NFT_ADDRESS"]
165 | with networks.ethereum[BLOCKCHAIN_NETWORK].use_provider(BLOCKCHAIN_PROVIDER):
166 | ct = ContractType.parse_obj({"abi": abi})
167 | NFTSmartContract = Contract(nft_address, ct)
168 | own_nft = NFTSmartContract.balanceOf(current_user.address) > 0
169 |
170 | if own_nft:
171 | return {"content": "Premium content"}
172 | else:
173 | return {"content": "Basic content"}
174 |
--------------------------------------------------------------------------------
/chapter_17/token-gated-blog/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useAccount, useConnect, useDisconnect, useSignMessage } from 'wagmi'
2 | import { useState, useEffect } from 'react';
3 |
4 | function App() {
5 | const account = useAccount()
6 | const { connectors, connect, status, error } = useConnect()
7 | const { disconnect } = useDisconnect()
8 | const { signMessageAsync } = useSignMessage()
9 | const message = `
10 | packtpub.com wants you to sign in with your Ethereum account:
11 | $address
12 |
13 | I accept the PacktPub Terms of Service: https://www.packtpub.com/en-us/help/terms-and-conditions
14 |
15 | URI: http://127.0.0.1:8000/token
16 | Version: 1
17 | Chain ID: 1
18 | Nonce: $nonce
19 | Issued At: $time
20 | Resources:
21 | - https://github.com/PacktPublishing/Hands-On-Blockchain-for-Python-Developers--2nd-Edition
22 | `;
23 |
24 | const [nonce, setNonce] = useState('')
25 | const [nonceTime, setNonceTime] = useState('')
26 | const [token, setToken] = useState('')
27 | const [signature, setSignature] = useState('')
28 | const [profile, setProfile] = useState('')
29 | const [content, setContent] = useState('')
30 | const fetchNonce = async () => {
31 | try {
32 | const address = account.addresses[0]
33 | const response = await fetch(`http://localhost:8000/nonce/${address}`,
34 | {method: 'GET',
35 | headers: {'Content-Type': 'application/json'},
36 | })
37 | const nonceData = await response.json()
38 | const nonceValue = nonceData.nonce
39 | const nonceTimeValue = nonceData.nonce_time
40 |
41 | setNonce(nonceValue)
42 | setNonceTime(nonceTimeValue)
43 | } catch (error) {
44 | console.error('Error fetching nonce:', error)
45 | }
46 | };
47 |
48 |
49 | return (
50 | <>
51 |