Skip to content

Commit 6b66b91

Browse files
committed
feat: Add utility methods for 100% JavaScript SDK parity
- Add parse_authorization_header() method for extracting DID tokens from HTTP headers - Add validate_token_ownership() method for NFT ownership validation (Token Gating) - Add ExpectedBearerStringError for invalid Authorization header formats - Add pyproject.toml for modern build configuration - Update version to 2.1.0 - Achieve 100% feature parity with JavaScript SDK
1 parent 6be724a commit 6b66b91

File tree

8 files changed

+166
-3
lines changed

8 files changed

+166
-3
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ repos:
1818
- id: codespell
1919
exclude: ^locale/
2020
- repo: https://github.com/astral-sh/ruff-pre-commit
21-
rev: v0.12.9
21+
rev: v0.12.10
2222
hooks:
2323
- id: ruff-check
2424
args: [--fix]

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
## `2.1.0` - 2025-08-27
2+
3+
#### 🚀 New Features
4+
5+
- **Added Utility Methods for 100% JavaScript SDK Parity**
6+
- `utils.parse_authorization_header(header)` - Extract DID tokens from HTTP Authorization headers
7+
- `utils.validate_token_ownership(did_token, contract_address, contract_type, rpc_url, token_id?)` - NFT ownership validation for Token Gating
8+
- Both methods match the exact functionality available in the JavaScript SDK
9+
10+
---
11+
112
## `2.0.1` - 2025-07-25
213

314
#### 🚀 Major Changes

magic_admin/error.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ class DIDTokenExpired(MagicError):
2828
pass
2929

3030

31+
class ExpectedBearerStringError(MagicError):
32+
pass
33+
34+
3135
class APIConnectionError(MagicError):
3236
pass
3337

magic_admin/resources/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from magic_admin.resources.token import Token # noqa: F401
22
from magic_admin.resources.user import User # noqa: F401
33
from magic_admin.resources.wallet import WalletType # noqa: F401
4+
from magic_admin.resources.utils import Utils # noqa: F401

magic_admin/resources/utils.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from magic_admin.resources.base import ResourceComponent
2+
from magic_admin.error import DIDTokenMalformed, DIDTokenExpired
3+
from magic_admin.error import ExpectedBearerStringError
4+
from web3 import Web3
5+
import json
6+
7+
8+
class Utils(ResourceComponent):
9+
"""
10+
Utility methods for Magic Admin SDK.
11+
"""
12+
13+
def parse_authorization_header(self, header: str) -> str:
14+
"""
15+
Parse a raw DID Token from the given Authorization header.
16+
17+
Args:
18+
header (str): The Authorization header string
19+
20+
Raises:
21+
ExpectedBearerStringError: If header is not in 'Bearer {token}' format
22+
23+
Returns:
24+
str: The DID token extracted from the header
25+
"""
26+
if not header.lower().startswith("bearer "):
27+
raise ExpectedBearerStringError(
28+
message="Expected argument to be a string in the `Bearer {token}` format."
29+
)
30+
31+
return header[7:] # Remove 'Bearer ' prefix
32+
33+
def validate_token_ownership(
34+
self,
35+
did_token: str,
36+
contract_address: str,
37+
contract_type: str,
38+
rpc_url: str,
39+
token_id: str = None,
40+
) -> dict:
41+
"""
42+
Token Gating function validates user ownership of wallet + NFT.
43+
44+
Args:
45+
did_token (str): The DID token to validate
46+
contract_address (str): The smart contract address
47+
contract_type (str): Either 'ERC721' or 'ERC1155'
48+
rpc_url (str): The RPC endpoint URL
49+
token_id (str, optional): Required for ERC1155 contracts
50+
51+
Raises:
52+
ValueError: If ERC1155 is specified without token_id
53+
Exception: If DID token validation fails
54+
55+
Returns:
56+
dict: Response with validation result
57+
{
58+
'valid': bool,
59+
'error_code': str,
60+
'message': str
61+
}
62+
"""
63+
# Make sure if ERC1155 has a tokenId
64+
if contract_type == "ERC1155" and not token_id:
65+
raise ValueError("ERC1155 requires a tokenId")
66+
67+
# Validate DID token
68+
try:
69+
self.Token.validate(did_token)
70+
wallet_address = self.Token.get_public_address(did_token)
71+
except DIDTokenMalformed:
72+
return {
73+
"valid": False,
74+
"error_code": "UNAUTHORIZED",
75+
"message": "Invalid DID token: ERROR_MALFORMED_TOKEN",
76+
}
77+
except DIDTokenExpired:
78+
return {
79+
"valid": False,
80+
"error_code": "UNAUTHORIZED",
81+
"message": "Invalid DID token: ERROR_DIDT_EXPIRED",
82+
}
83+
except Exception as e:
84+
raise Exception(str(e))
85+
86+
# Check on-chain if user owns NFT by calling contract with web3
87+
w3 = Web3(Web3.HTTPProvider(rpc_url))
88+
89+
if contract_type == "ERC721":
90+
# ERC721 ABI for balanceOf function
91+
abi = json.loads(
92+
'[{"constant":true,"inputs":[{"name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"type":"function"}]'
93+
)
94+
contract = w3.eth.contract(address=contract_address, abi=abi)
95+
balance = contract.functions.balanceOf(wallet_address).call()
96+
else: # ERC1155
97+
# ERC1155 ABI for balanceOf function
98+
abi = json.loads(
99+
'[{"constant":true,"inputs":[{"name":"owner","type":"address"},{"name":"id","type":"uint256"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"type":"function"}]'
100+
)
101+
contract = w3.eth.contract(address=contract_address, abi=abi)
102+
balance = contract.functions.balanceOf(wallet_address, int(token_id)).call()
103+
104+
if balance > 0:
105+
return {"valid": True, "error_code": "", "message": ""}
106+
107+
return {
108+
"valid": False,
109+
"error_code": "NO_OWNERSHIP",
110+
"message": "User does not own this token.",
111+
}

magic_admin/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = "2.0.1"
1+
VERSION = "2.1.0"

pyproject.toml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[build-system]
2+
requires = ["setuptools>=40.8.0", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "magic-admin"
7+
version = "2.1.0"
8+
description = "Magic Python Library"
9+
readme = "README.md"
10+
license = {text = "MIT"}
11+
authors = [
12+
{name = "Magic", email = "[email protected]"}
13+
]
14+
keywords = ["magic", "python", "sdk"]
15+
classifiers = [
16+
"Development Status :: 3 - Alpha",
17+
"Programming Language :: Python",
18+
"Programming Language :: Python :: 3.11",
19+
"Intended Audience :: Developers",
20+
"License :: OSI Approved :: MIT License",
21+
"Operating System :: OS Independent",
22+
]
23+
requires-python = ">=3.11"
24+
dependencies = [
25+
"requests==2.32.4",
26+
"web3==7.13.0",
27+
"websockets==15.0.1",
28+
]
29+
30+
[project.urls]
31+
Website = "https://magic.link"
32+
33+
[tool.setuptools]
34+
packages = ["magic_admin"]
35+
36+
[tool.setuptools.package-data]
37+
magic_admin = ["*.txt", "*.md"]

setup.cfg

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,4 @@
22
universal = 1
33

44
[metadata]
5-
license_file = LICENSE.txt
65
python_requires = >=3.11

0 commit comments

Comments
 (0)