Skip to content

Commit 38dafb3

Browse files
authored
Merge pull request #5 from devinaconley/hubs
hubs
2 parents 321906c + 3b8c190 commit 38dafb3

10 files changed

Lines changed: 187 additions & 36 deletions

File tree

examples/simple/api/index.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import os
55
import time
66
from flask import Flask, url_for, jsonify
7-
from framelib import frame, message, validate_message_or_mock_vercel
7+
from framelib import frame, message, validate_message_or_mock, validate_message_or_mock_neynar
88

99
app = Flask(__name__)
1010

@@ -38,14 +38,18 @@ def second_page():
3838

3939
# validate frame message with neynar
4040
api_key = os.getenv('NEYNAR_KEY')
41-
msg_val = validate_message_or_mock_vercel(msg, api_key)
42-
print(f'validated frame message, fid: {msg_val.interactor.fid}, button: {msg_val.tapped_button}')
41+
msg_neynar = validate_message_or_mock_neynar(msg, api_key, mock=_vercel_local())
42+
print(f'validated frame message, fid: {msg_neynar.interactor.fid}, button: {msg_neynar.tapped_button}')
43+
44+
# validate frame message with hub
45+
msg_hub = validate_message_or_mock(msg, 'https://nemes.farcaster.xyz:2281', mock=_vercel_local())
46+
print(f'validated frame message hub, fid: {msg_hub.data.fid}, button: {msg_hub.data.frameActionBody.buttonIndex}')
4347

4448
return frame(
4549
image=_github_preview_image(),
4650
button1='back \U0001F519',
4751
post_url=url_for('home', _external=True),
48-
input_text=f'hello {msg_val.interactor.username}!',
52+
input_text=f'hello {msg_neynar.interactor.username}!',
4953
button2='github',
5054
button2_action='link',
5155
button2_target='https://github.com/devinaconley/python-frames'
@@ -55,3 +59,8 @@ def second_page():
5559
def _github_preview_image() -> str:
5660
hour = int((time.time() // 3600) * 3600) # github throttles if you invalidate image cache too much
5761
return f'https://opengraph.githubassets.com/{hour}/devinaconley/python-frames'
62+
63+
64+
def _vercel_local() -> bool:
65+
vercel_env = os.getenv('VERCEL_ENV')
66+
return vercel_env is None or vercel_env == 'development'

examples/simple/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# requirements.txt
2-
framelib~=0.0.3
2+
framelib~=0.0.4b3
33
Flask~=3.0.1
44
pydantic

examples/transaction/api/index.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import json
66
from flask import Flask, url_for, jsonify, request
7-
from framelib import frame, message, validate_message_or_mock_vercel, transaction
7+
from framelib import frame, message, transaction
88
from eth_utils import to_wei
99

1010
from .constant import ABI_WETH, CHAIN_ID, ADDRESS_WETH, IM_WETH
@@ -36,7 +36,8 @@ def home():
3636
button2_action='tx',
3737
button2_target=url_for('tx_withdraw', _external=True, value=to_wei(0.01, 'ether')),
3838
input_text='WETH amount',
39-
post_url=url_for('home', _external=True)
39+
post_url=url_for('home', _external=True),
40+
max_age=3600
4041
)
4142

4243

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# requirements.txt
2-
framelib~=0.0.3
2+
framelib~=0.0.4b3
33
Flask~=3.0.1
44
pydantic

framelib/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
"""
44

55
from .frame import frame, message
6+
from .hub import validate_message, validate_message_or_mock
67
from .models import FrameMessage, ValidatedMessage, User
78
from .warpcast import get_user
8-
from .neynar import validate_message, validate_message_or_mock, validate_message_or_mock_vercel
9+
from .neynar import (
10+
validate_message as validate_message_neynar,
11+
validate_message_or_mock as validate_message_or_mock_neynar
12+
)
913
from .transaction import transaction

framelib/hub.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""
2+
methods to interact with farcaster hub
3+
"""
4+
5+
# lib
6+
import os
7+
import requests
8+
9+
# src
10+
from .models import FrameMessage, ValidatedMessage, ValidatedData, FrameAction, CastId
11+
12+
13+
def get_message(
14+
msg: str,
15+
hub: str,
16+
username: str = None,
17+
password: str = None,
18+
api_key: str = None
19+
) -> ValidatedMessage:
20+
url = f'{hub}/v1/validateMessage'
21+
headers = {'content-type': 'application/octet-stream'}
22+
if api_key:
23+
headers['api_key'] = api_key
24+
auth = None
25+
if username:
26+
auth = (username, password)
27+
res = requests.post(url, headers=headers, auth=auth, data=bytes.fromhex(msg))
28+
29+
body = res.json()
30+
if not body['valid']:
31+
raise ValueError('frame action message is invalid')
32+
action = ValidatedMessage(**body['message'])
33+
34+
return action
35+
36+
37+
def validate_message(
38+
msg: FrameMessage,
39+
hub: str,
40+
username: str = None,
41+
password: str = None,
42+
api_key: str = None
43+
) -> ValidatedMessage:
44+
action = get_message(msg.trustedData.messageBytes, hub, username=username, password=password, api_key=api_key)
45+
46+
if msg.untrustedData.fid != action.data.fid:
47+
raise ValueError(f'fid does not match: {msg.untrustedData.fid} {action.data.fid}')
48+
49+
if msg.untrustedData.buttonIndex != action.data.frameActionBody.buttonIndex:
50+
raise ValueError(
51+
f'button index does not match: {msg.untrustedData.buttonIndex} {action.data.frameActionBody.buttonIndex}')
52+
53+
if msg.untrustedData.inputText is not None and msg.untrustedData.inputText != action.data.frameActionBody.inputText:
54+
raise ValueError(
55+
f'text input does not match: {msg.untrustedData.inputText} {action.data.frameActionBody.inputText}')
56+
57+
if msg.untrustedData.state is not None and msg.untrustedData.state != action.data.frameActionBody.state:
58+
raise ValueError(f'state does not match: {msg.untrustedData.state} {action.data.frameActionBody.state}')
59+
60+
return action
61+
62+
63+
def validate_message_or_mock(
64+
msg: FrameMessage,
65+
hub: str,
66+
username: str = None,
67+
password: str = None,
68+
api_key: str = None,
69+
mock: bool = False
70+
) -> ValidatedMessage:
71+
if mock:
72+
# mock
73+
return ValidatedMessage(
74+
data=ValidatedData(
75+
type='MESSAGE_TYPE_FRAME_ACTION',
76+
fid=msg.untrustedData.fid,
77+
timestamp=msg.untrustedData.timestamp,
78+
network=str(msg.untrustedData.network),
79+
frameActionBody=FrameAction(
80+
url=msg.untrustedData.url,
81+
buttonIndex=msg.untrustedData.buttonIndex,
82+
castId=msg.untrustedData.castId,
83+
inputTest=msg.untrustedData.inputText,
84+
state=msg.untrustedData.state,
85+
transactionId=msg.untrustedData.transactionId
86+
)
87+
),
88+
hash=msg.untrustedData.messageHash,
89+
hashScheme='HASH_SCHEME_BLAKE',
90+
signature='',
91+
signatureScheme='SIGNATURE_SCHEME_ED25519',
92+
signer=''
93+
)
94+
95+
return validate_message(msg, hub, username=username, password=password, api_key=api_key)

framelib/models.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,23 @@ class CastId(BaseModel):
1414
hash: str
1515

1616

17-
class UntrustedData(BaseModel):
18-
fid: int
17+
class FrameAction(BaseModel):
1918
url: str
20-
messageHash: str
21-
timestamp: int
22-
network: int
2319
buttonIndex: int
2420
inputText: Optional[str] = None
2521
state: Optional[str] = None
2622
transactionId: Optional[str] = None
2723
castId: CastId
2824

2925

26+
class UntrustedData(FrameAction):
27+
# note: this untrusted message seems to collapse the ValidatedDate and FrameAction fields
28+
fid: int
29+
messageHash: str
30+
timestamp: int
31+
network: int
32+
33+
3034
class TrustedData(BaseModel):
3135
messageBytes: str
3236

@@ -51,6 +55,25 @@ class Transaction(BaseModel):
5155
params: EthTransactionParams
5256

5357

58+
# ---- hub ----
59+
60+
class ValidatedData(BaseModel):
61+
type: str
62+
fid: int
63+
timestamp: datetime.datetime
64+
network: str
65+
frameActionBody: FrameAction
66+
67+
68+
class ValidatedMessage(BaseModel):
69+
data: ValidatedData
70+
hash: str
71+
hashScheme: str
72+
signature: str
73+
signatureScheme: str
74+
signer: str
75+
76+
5477
# ---- neynar ----
5578

5679
class NeynarViewer(BaseModel):
@@ -67,7 +90,7 @@ class NeynarProfile(BaseModel):
6790
bio: NeynarBio
6891

6992

70-
class Interactor(BaseModel):
93+
class NeynarInteractor(BaseModel):
7194
object: str
7295
fid: int
7396
username: str
@@ -100,9 +123,9 @@ class NeynarTransaction(BaseModel):
100123
hash: str
101124

102125

103-
class ValidatedMessage(BaseModel):
126+
class NeynarValidatedMessage(BaseModel):
104127
object: str
105-
interactor: Interactor
128+
interactor: NeynarInteractor
106129
tapped_button: NeynarButton
107130
input: Optional[NeynarInput] = None
108131
state: Optional[NeynarState] = None

framelib/neynar.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
"""
22
methods to call neynar api
33
"""
4-
import os
54

5+
# lib
6+
import os
67
import requests
78

8-
from .models import FrameMessage, ValidatedMessage, Interactor, NeynarProfile, NeynarBio, NeynarButton, NeynarInput, \
9-
NeynarState, NeynarTransaction
9+
# src
10+
from .models import FrameMessage, NeynarValidatedMessage, NeynarInteractor, NeynarProfile, NeynarBio, \
11+
NeynarButton, NeynarInput, NeynarState, NeynarTransaction
1012

1113

12-
def get_frame_action(msg: str, api_key: str) -> ValidatedMessage:
14+
def get_frame_message(msg: str, api_key: str) -> NeynarValidatedMessage:
1315
if not api_key:
1416
raise ValueError('neynar api key not set')
1517
url = 'https://api.neynar.com/v2/farcaster/frame/validate'
@@ -30,13 +32,13 @@ def get_frame_action(msg: str, api_key: str) -> ValidatedMessage:
3032
if not body['valid']:
3133
raise ValueError('frame action message is invalid')
3234

33-
action = ValidatedMessage(**body['action'])
35+
action = NeynarValidatedMessage(**body['action'])
3436

3537
return action
3638

3739

38-
def validate_message(msg: FrameMessage, api_key: str) -> ValidatedMessage:
39-
action = get_frame_action(msg.trustedData.messageBytes, api_key)
40+
def validate_message(msg: FrameMessage, api_key: str) -> NeynarValidatedMessage:
41+
action = get_frame_message(msg.trustedData.messageBytes, api_key)
4042

4143
if msg.untrustedData.fid != action.interactor.fid:
4244
raise ValueError(f'fid does not match: {msg.untrustedData.fid} {action.interactor.fid}')
@@ -53,13 +55,13 @@ def validate_message(msg: FrameMessage, api_key: str) -> ValidatedMessage:
5355
return action
5456

5557

56-
def validate_message_or_mock(msg: FrameMessage, api_key: str, mock: bool = False) -> ValidatedMessage:
58+
def validate_message_or_mock(msg: FrameMessage, api_key: str, mock: bool = False) -> NeynarValidatedMessage:
5759
if mock:
5860
# mock
5961
# TODO option to populate with warpcast profile
60-
return ValidatedMessage(
62+
return NeynarValidatedMessage(
6163
object='validated_frame_action',
62-
interactor=Interactor(
64+
interactor=NeynarInteractor(
6365
object='user',
6466
fid=msg.untrustedData.fid,
6567
username=f'username {msg.untrustedData.fid}',
@@ -81,8 +83,3 @@ def validate_message_or_mock(msg: FrameMessage, api_key: str, mock: bool = False
8183
)
8284

8385
return validate_message(msg, api_key)
84-
85-
86-
def validate_message_or_mock_vercel(msg: FrameMessage, api_key: str) -> ValidatedMessage:
87-
vercel_env = os.getenv('VERCEL_ENV')
88-
return validate_message_or_mock(msg, api_key, vercel_env is None or vercel_env == 'development')

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setuptools.setup(
77
name='framelib',
8-
version='0.0.3',
8+
version='0.0.4b3',
99
author='Devin A. Conley',
1010
author_email='devinaconley@gmail.com',
1111
description='lightweight library for building farcaster frames using python and flask',

test/test_validation.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,37 @@
77

88
# src
99
from framelib import validate_message
10-
from framelib.neynar import get_frame_action
10+
from framelib.neynar import get_frame_message
11+
from framelib.hub import get_message
1112

1213

13-
class TestValidateMessage(object):
14+
class TestValidateMessageNeynar(object):
1415

1516
def test_button_1(self):
1617
# example signed message from fid 8268 clicking button 1
1718
msg = '0a4e080d10cc4018cbe1a230200182013f0a2068747470733a2f2f707974686f6e2d6672616d652e76657263656c2e6170702f10011a1908cc401214000000000000000000000000000000000000000112140101bf04a2e61cb24c9a66c047ac5ed175e1bed8180122403feee9d0c1392c1e5bc7bca49850f83735c53b4f60c88959ffc271123e333a196e963d15619125e6034acda36076c709182daa5625e4affe6df21866c204830828013220ad4520314a78bc4317c604a3324ebc25bd8215c3ac38342fd790b7905c291bd1'
18-
action = get_frame_action(msg, 'NEYNAR_API_DOCS')
19+
action = get_frame_message(msg, 'NEYNAR_API_DOCS')
1920
assert action.tapped_button.index == 1
2021
assert action.interactor.fid == 8268
2122
assert action.input is None
23+
24+
25+
class TestValidateMessageHub(object):
26+
27+
def test_button_1(self):
28+
# example signed message from fid 8268 clicking button 1
29+
msg = '0a4e080d10cc4018cbe1a230200182013f0a2068747470733a2f2f707974686f6e2d6672616d652e76657263656c2e6170702f10011a1908cc401214000000000000000000000000000000000000000112140101bf04a2e61cb24c9a66c047ac5ed175e1bed8180122403feee9d0c1392c1e5bc7bca49850f83735c53b4f60c88959ffc271123e333a196e963d15619125e6034acda36076c709182daa5625e4affe6df21866c204830828013220ad4520314a78bc4317c604a3324ebc25bd8215c3ac38342fd790b7905c291bd1'
30+
action = get_message(msg, 'https://nemes.farcaster.xyz:2281') # public read only
31+
assert action.data.frameActionBody.buttonIndex == 1
32+
assert action.data.fid == 8268
33+
assert action.data.network == 'FARCASTER_NETWORK_MAINNET'
34+
assert action.data.frameActionBody.inputText == ''
35+
36+
def test_button_1_neynar(self):
37+
# example signed message from fid 8268 clicking button 1
38+
msg = '0a4e080d10cc4018cbe1a230200182013f0a2068747470733a2f2f707974686f6e2d6672616d652e76657263656c2e6170702f10011a1908cc401214000000000000000000000000000000000000000112140101bf04a2e61cb24c9a66c047ac5ed175e1bed8180122403feee9d0c1392c1e5bc7bca49850f83735c53b4f60c88959ffc271123e333a196e963d15619125e6034acda36076c709182daa5625e4affe6df21866c204830828013220ad4520314a78bc4317c604a3324ebc25bd8215c3ac38342fd790b7905c291bd1'
39+
action = get_message(msg, 'https://hub-api.neynar.com', api_key='NEYNAR_API_DOCS')
40+
assert action.data.frameActionBody.buttonIndex == 1
41+
assert action.data.fid == 8268
42+
assert action.data.network == 'FARCASTER_NETWORK_MAINNET'
43+
assert action.data.frameActionBody.inputText == ''

0 commit comments

Comments
 (0)