algokit-subscriber 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- algokit_subscriber-1.0.1/PKG-INFO +234 -0
- algokit_subscriber-1.0.1/README.md +221 -0
- algokit_subscriber-1.0.1/pyproject.toml +176 -0
- algokit_subscriber-1.0.1/src/algokit_subscriber/__init__.py +28 -0
- algokit_subscriber-1.0.1/src/algokit_subscriber/block.py +52 -0
- algokit_subscriber-1.0.1/src/algokit_subscriber/indexer_lookup.py +176 -0
- algokit_subscriber-1.0.1/src/algokit_subscriber/subscriber.py +215 -0
- algokit_subscriber-1.0.1/src/algokit_subscriber/subscription.py +1240 -0
- algokit_subscriber-1.0.1/src/algokit_subscriber/transform.py +1009 -0
- algokit_subscriber-1.0.1/src/algokit_subscriber/types/__init__.py +0 -0
- algokit_subscriber-1.0.1/src/algokit_subscriber/types/arc28.py +90 -0
- algokit_subscriber-1.0.1/src/algokit_subscriber/types/block.py +320 -0
- algokit_subscriber-1.0.1/src/algokit_subscriber/types/event_emitter.py +68 -0
- algokit_subscriber-1.0.1/src/algokit_subscriber/types/indexer.py +437 -0
- algokit_subscriber-1.0.1/src/algokit_subscriber/types/subscription.py +367 -0
- algokit_subscriber-1.0.1/src/algokit_subscriber/types/transaction.py +110 -0
- algokit_subscriber-1.0.1/src/algokit_subscriber/utils.py +29 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: algokit-subscriber
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary:
|
|
5
|
+
Author: Algorand Foundation
|
|
6
|
+
Requires-Python: >=3.12,<4.0
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Requires-Dist: py-algorand-sdk (>=2.9.1,<3.0.0)
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
<div align="center">
|
|
14
|
+
<a href="https://github.com/algorandfoundation/algokit-subscriber-py"><img src="https://bafybeidbb3a7cgn3unoz4oouk2jme4eavqgqtnskfr4bqhbjku3s52de4a.ipfs.w3s.link/algokit-subscriber-logo.png" width=60%></a>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<a target="_blank" href="https://algorandfoundation.github.io/algokit-subscriber-py/"><img src="https://img.shields.io/badge/docs-repository-74dfdc?logo=github&style=flat.svg" /></a>
|
|
19
|
+
<a target="_blank" href="https://algorand.co/algokit"><img src="https://img.shields.io/badge/learn-AlgoKit-74dfdc?logo=algorand&mac=flat.svg" /></a>
|
|
20
|
+
<a target="_blank" href="https://github.com/algorandfoundation/algokit-subscriber-py"><img src="https://img.shields.io/github/stars/algorandfoundation/algokit-subscriber-py?color=74dfdc&logo=star&style=flat" /></a>
|
|
21
|
+
<a target="_blank" href="https://algorand.co/algokit"><img src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Fgithub.com%2Falgorandfoundation%2Falgokit-subscriber-py&countColor=%2374dfdc&style=flat" /></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
This library a simple, but flexible / configurable Algorand transaction subscription / indexing mechanism. It allows you to quickly create Python services that follow or subscribe to the Algorand Blockchain.
|
|
27
|
+
|
|
28
|
+
> pip install algokit_subscriber
|
|
29
|
+
|
|
30
|
+
[Documentation](./docs/index.md)
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
# Create subscriber
|
|
36
|
+
subscriber = AlgorandSubscriber(
|
|
37
|
+
{
|
|
38
|
+
"filters": [
|
|
39
|
+
{
|
|
40
|
+
"name": "filter1",
|
|
41
|
+
"filter": {
|
|
42
|
+
"type": "pay",
|
|
43
|
+
"sender": "ABC...",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
# ... other options (use intellisense to explore)
|
|
48
|
+
},
|
|
49
|
+
algod,
|
|
50
|
+
optional_indexer
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
# Set up subscription(s)
|
|
54
|
+
def on_filter1(transaction, event_name):
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
subscriber.on("filter1", on_filter1)
|
|
58
|
+
|
|
59
|
+
# Either: Start the subscriber (if in long-running process)
|
|
60
|
+
subscriber.start();
|
|
61
|
+
|
|
62
|
+
# OR: Poll the subscriber (if in cron job / periodic lambda)
|
|
63
|
+
subscriber.pollOnce();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Key features
|
|
67
|
+
|
|
68
|
+
- **Notification _and_ indexing** - You have fine-grained control over the syncing behaviour and can control the number of rounds to sync at a time, the pattern of syncing i.e. start from the beginning of the chain, or start from the tip; drop stale records if your service can't keep up or keep syncing from where you are up to; etc.
|
|
69
|
+
- **Low latency processing** - When your service has caught up to the tip of the chain it can optionally wait for new rounds so you have a low latency reaction to a new round occurring
|
|
70
|
+
- **Watermarking and resilience** - You can create reliable syncing / indexing services through a simple round watermarking capability that allows you to create resilient syncing services that can recover from an outage
|
|
71
|
+
- **Extensive subscription filtering** - You can filter by transaction type, sender, receiver, note prefix, apps (ID, creation, on complete, ARC-4 method signature, call arguments, ARC-28 events), assets (ID, creation, amount transferred range), transfers (amount transferred range) and balance changes (algo and assets)
|
|
72
|
+
- **ARC-28 event subscription support** - You can subscribe to ARC-28 events for a smart contract
|
|
73
|
+
- **Balance change support** - Subscribed transactions will have all algo and asset balance changes calculated for you and you can also subscribe to balance changes that meet certain criteria
|
|
74
|
+
- **First-class inner transaction support** - Your filter will find arbitrarily nested inner transactions and return that transaction (indexer can't do this!)
|
|
75
|
+
- **State-proof support** - You can subscribe to state proof transactions
|
|
76
|
+
- **Simple programming model** - It's really easy to use and consume through easy to use, type-safe methods and objects and subscribed transactions have a comprehensive and familiar model type with all relevant/useful information about that transaction (including things like transaction id, round number, created asset/app id, app logs, etc.) modelled on the indexer data model (which is used regardless of whether the transactions come from indexer or algod so it's a consistent experience)
|
|
77
|
+
- **Easy to deploy** - You have full control over how you want to deploy and use the subscriber; it will work with whatever persistence (e.g. sql, no-sql, etc.), queuing/messaging (e.g. queues, topics, buses, web hooks, web sockets) and compute (e.g. serverless periodic lambdas, continually running containers, virtual machines, etc.) services you want to use
|
|
78
|
+
- **Fast initial index** - There is an indexer catch up mode that allows you to use indexer to catch up to the tip of the chain in seconds or minutes rather than days; alternatively, if you prefer to just use algod and not indexer that option is available too!
|
|
79
|
+
|
|
80
|
+
## Balance change notes
|
|
81
|
+
|
|
82
|
+
The balance change semantics work mostly as expected, however the sematics around asset creation and destruction warrants further clarification.
|
|
83
|
+
|
|
84
|
+
When an asset is created, the full asset supply is attributed to the asset creators account.
|
|
85
|
+
|
|
86
|
+
The balance change for an asset create transaction will be as below:
|
|
87
|
+
|
|
88
|
+
```py
|
|
89
|
+
{
|
|
90
|
+
"address": "VIDHG4SYANCP2GUQXXSFSNBPJWS4TAQSI3GH4GYO54FSYPDIBYPMSF7HBY", # The asset creator
|
|
91
|
+
"asset_id": 2391, # The created asset id
|
|
92
|
+
"amount": 100000, # Full asset supply of the created asset
|
|
93
|
+
"roles": [BalanceChangeroles.AssetCreator]
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
When an asset is destroyed, the full asset supply must be in the asset creators account and the asset manager must send the destroy transaction.
|
|
98
|
+
Unfortunately we cannot determine the asset creator or full asset supply from the transaction data. As a result the balance change will always be attributed to the asset manager and will have a 0 amount.
|
|
99
|
+
If you need to account for the asset supply being destroyed from the creators account, you'll need to handle this separately.
|
|
100
|
+
|
|
101
|
+
The balance change for an asset destroy transaction will be as below:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
{
|
|
105
|
+
"address": "PIDHG4SYANCP2GUQXXSFSNBPJWS4TAQSI3GH4GYO54FSYPDIBYPMSF7HBY", # The asset destroyer, which will always be the asset manager
|
|
106
|
+
"assetId": 2391, # The destroyed asset id
|
|
107
|
+
"amount": 0, # This value will always be 0
|
|
108
|
+
"roles": [BalanceChangeroles.AssetDestroyer]
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Examples
|
|
113
|
+
|
|
114
|
+
### Data History Museum index
|
|
115
|
+
|
|
116
|
+
The following code, when algod is pointed to TestNet, will find all transactions emitted by the [Data History Museum](https://datahistory.org) since the beginning of time in _seconds_ and then find them in real-time as they emerge on the chain.
|
|
117
|
+
|
|
118
|
+
The watermark is stored in-memory so this particular example is not resilient to restarts. To change that you can implement proper persistence of the watermark. There is [an example that uses the file system](./examples/data-history-museum/) to demonstrate this.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
algorand = AlgorandClient.testnet()
|
|
122
|
+
|
|
123
|
+
# The watermark is used to track how far the subscriber has processed transactions
|
|
124
|
+
watermark = 0
|
|
125
|
+
|
|
126
|
+
def get_watermark() -> int:
|
|
127
|
+
return watermark
|
|
128
|
+
|
|
129
|
+
def set_watermark(new_watermark: int) -> None:
|
|
130
|
+
global watermark
|
|
131
|
+
watermark = new_watermark
|
|
132
|
+
|
|
133
|
+
subscriber = AlgorandSubscriber(
|
|
134
|
+
# algod is used to get the latest transactions once the subscriber has caught up to the network
|
|
135
|
+
algod_client=algorand.client.algod,
|
|
136
|
+
config={
|
|
137
|
+
"filters": [
|
|
138
|
+
{
|
|
139
|
+
"name": "dhm-asset",
|
|
140
|
+
"filter": {
|
|
141
|
+
# Match asset configuration transactions
|
|
142
|
+
"type": "acfg",
|
|
143
|
+
# Data History Museum creator account on TestNet
|
|
144
|
+
"sender": "ER7AMZRPD5KDVFWTUUVOADSOWM4RQKEEV2EDYRVSA757UHXOIEKGMBQIVU",
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
"frequency_in_seconds": 5,
|
|
149
|
+
"max_rounds_to_sync": 100,
|
|
150
|
+
"sync_behaviour": "catchup-with-indexer",
|
|
151
|
+
"watermark_persistence": {"get": get_watermark, "set": set_watermark},
|
|
152
|
+
},
|
|
153
|
+
# indexer is used to get historical transactions
|
|
154
|
+
indexer_client=algorand.client.indexer,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def process_dhm_assets(transactions: list[SubscribedTransaction], filter_name: str) -> None:
|
|
158
|
+
print(f"Received {len(transactions)} asset changes")
|
|
159
|
+
# ... do stuff with the transactions
|
|
160
|
+
|
|
161
|
+
# Attach our callback to the 'dhm-asset' filter
|
|
162
|
+
subscriber.on_batch("dhm-asset", process_dhm_assets)
|
|
163
|
+
|
|
164
|
+
def handle_error(error: Exception) -> None:
|
|
165
|
+
print(f"An error occurred: {error}")
|
|
166
|
+
|
|
167
|
+
# Attach the error handler
|
|
168
|
+
subscriber.on_error(handle_error)
|
|
169
|
+
|
|
170
|
+
# Start the subscriber
|
|
171
|
+
subscriber.start()
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### USDC real-time monitoring
|
|
175
|
+
|
|
176
|
+
The following code, when algod is pointed to MainNet, will find all transfers of [USDC](https://www.circle.com/en/usdc-multichain/algorand) that are greater than $1 and it will poll every 1s for new transfers.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from algokit_subscriber import AlgorandSubscriber
|
|
180
|
+
from algokit_subscriber.types import SubscribedTransaction
|
|
181
|
+
from algokit_utils import AlgorandClient
|
|
182
|
+
|
|
183
|
+
algorand = AlgorandClient.mainnet()
|
|
184
|
+
|
|
185
|
+
# The watermark is used to track how far the subscriber has processed transactions
|
|
186
|
+
watermark = 0
|
|
187
|
+
|
|
188
|
+
def get_watermark() -> int:
|
|
189
|
+
return watermark
|
|
190
|
+
|
|
191
|
+
def set_watermark(new_watermark: int) -> None:
|
|
192
|
+
global watermark
|
|
193
|
+
watermark = new_watermark
|
|
194
|
+
|
|
195
|
+
subscriber = AlgorandSubscriber(
|
|
196
|
+
algod_client=algorand.client.algod,
|
|
197
|
+
config={
|
|
198
|
+
"filters": [
|
|
199
|
+
{
|
|
200
|
+
"name": "usdc",
|
|
201
|
+
"filter": {
|
|
202
|
+
"type": "axfer",
|
|
203
|
+
"asset_id": 31566704, # MainNet: USDC
|
|
204
|
+
"min_amount": 1_000_000, # $1
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
],
|
|
208
|
+
"wait_for_block_when_at_tip": True,
|
|
209
|
+
"sync_behaviour": "skip-sync-newest",
|
|
210
|
+
"watermark_persistence": {"get": get_watermark, "set": set_watermark},
|
|
211
|
+
},
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def process_usdc_transfer(transfer: SubscribedTransaction, filter_name: str) -> None:
|
|
215
|
+
asset_transfer = transfer.get("asset-transfer-transaction", {})
|
|
216
|
+
amount = asset_transfer.get("amount", 0) / 1_000_000
|
|
217
|
+
print(
|
|
218
|
+
f"{transfer['sender']} sent {asset_transfer.get('receiver')} "
|
|
219
|
+
f"USDC${amount:.2f} in transaction {transfer['id']}"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Attach our callback to the 'usdc' filter
|
|
223
|
+
subscriber.on("usdc", process_usdc_transfer)
|
|
224
|
+
|
|
225
|
+
def handle_error(error: Exception) -> None:
|
|
226
|
+
print(f"An error occurred: {error}")
|
|
227
|
+
|
|
228
|
+
# Attach the error handler
|
|
229
|
+
subscriber.on_error(handle_error)
|
|
230
|
+
|
|
231
|
+
# Start the subscriber
|
|
232
|
+
subscriber.start()
|
|
233
|
+
```
|
|
234
|
+
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://github.com/algorandfoundation/algokit-subscriber-py"><img src="https://bafybeidbb3a7cgn3unoz4oouk2jme4eavqgqtnskfr4bqhbjku3s52de4a.ipfs.w3s.link/algokit-subscriber-logo.png" width=60%></a>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a target="_blank" href="https://algorandfoundation.github.io/algokit-subscriber-py/"><img src="https://img.shields.io/badge/docs-repository-74dfdc?logo=github&style=flat.svg" /></a>
|
|
7
|
+
<a target="_blank" href="https://algorand.co/algokit"><img src="https://img.shields.io/badge/learn-AlgoKit-74dfdc?logo=algorand&mac=flat.svg" /></a>
|
|
8
|
+
<a target="_blank" href="https://github.com/algorandfoundation/algokit-subscriber-py"><img src="https://img.shields.io/github/stars/algorandfoundation/algokit-subscriber-py?color=74dfdc&logo=star&style=flat" /></a>
|
|
9
|
+
<a target="_blank" href="https://algorand.co/algokit"><img src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Fgithub.com%2Falgorandfoundation%2Falgokit-subscriber-py&countColor=%2374dfdc&style=flat" /></a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
This library a simple, but flexible / configurable Algorand transaction subscription / indexing mechanism. It allows you to quickly create Python services that follow or subscribe to the Algorand Blockchain.
|
|
15
|
+
|
|
16
|
+
> pip install algokit_subscriber
|
|
17
|
+
|
|
18
|
+
[Documentation](./docs/index.md)
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
# Create subscriber
|
|
24
|
+
subscriber = AlgorandSubscriber(
|
|
25
|
+
{
|
|
26
|
+
"filters": [
|
|
27
|
+
{
|
|
28
|
+
"name": "filter1",
|
|
29
|
+
"filter": {
|
|
30
|
+
"type": "pay",
|
|
31
|
+
"sender": "ABC...",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
# ... other options (use intellisense to explore)
|
|
36
|
+
},
|
|
37
|
+
algod,
|
|
38
|
+
optional_indexer
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
# Set up subscription(s)
|
|
42
|
+
def on_filter1(transaction, event_name):
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
subscriber.on("filter1", on_filter1)
|
|
46
|
+
|
|
47
|
+
# Either: Start the subscriber (if in long-running process)
|
|
48
|
+
subscriber.start();
|
|
49
|
+
|
|
50
|
+
# OR: Poll the subscriber (if in cron job / periodic lambda)
|
|
51
|
+
subscriber.pollOnce();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Key features
|
|
55
|
+
|
|
56
|
+
- **Notification _and_ indexing** - You have fine-grained control over the syncing behaviour and can control the number of rounds to sync at a time, the pattern of syncing i.e. start from the beginning of the chain, or start from the tip; drop stale records if your service can't keep up or keep syncing from where you are up to; etc.
|
|
57
|
+
- **Low latency processing** - When your service has caught up to the tip of the chain it can optionally wait for new rounds so you have a low latency reaction to a new round occurring
|
|
58
|
+
- **Watermarking and resilience** - You can create reliable syncing / indexing services through a simple round watermarking capability that allows you to create resilient syncing services that can recover from an outage
|
|
59
|
+
- **Extensive subscription filtering** - You can filter by transaction type, sender, receiver, note prefix, apps (ID, creation, on complete, ARC-4 method signature, call arguments, ARC-28 events), assets (ID, creation, amount transferred range), transfers (amount transferred range) and balance changes (algo and assets)
|
|
60
|
+
- **ARC-28 event subscription support** - You can subscribe to ARC-28 events for a smart contract
|
|
61
|
+
- **Balance change support** - Subscribed transactions will have all algo and asset balance changes calculated for you and you can also subscribe to balance changes that meet certain criteria
|
|
62
|
+
- **First-class inner transaction support** - Your filter will find arbitrarily nested inner transactions and return that transaction (indexer can't do this!)
|
|
63
|
+
- **State-proof support** - You can subscribe to state proof transactions
|
|
64
|
+
- **Simple programming model** - It's really easy to use and consume through easy to use, type-safe methods and objects and subscribed transactions have a comprehensive and familiar model type with all relevant/useful information about that transaction (including things like transaction id, round number, created asset/app id, app logs, etc.) modelled on the indexer data model (which is used regardless of whether the transactions come from indexer or algod so it's a consistent experience)
|
|
65
|
+
- **Easy to deploy** - You have full control over how you want to deploy and use the subscriber; it will work with whatever persistence (e.g. sql, no-sql, etc.), queuing/messaging (e.g. queues, topics, buses, web hooks, web sockets) and compute (e.g. serverless periodic lambdas, continually running containers, virtual machines, etc.) services you want to use
|
|
66
|
+
- **Fast initial index** - There is an indexer catch up mode that allows you to use indexer to catch up to the tip of the chain in seconds or minutes rather than days; alternatively, if you prefer to just use algod and not indexer that option is available too!
|
|
67
|
+
|
|
68
|
+
## Balance change notes
|
|
69
|
+
|
|
70
|
+
The balance change semantics work mostly as expected, however the sematics around asset creation and destruction warrants further clarification.
|
|
71
|
+
|
|
72
|
+
When an asset is created, the full asset supply is attributed to the asset creators account.
|
|
73
|
+
|
|
74
|
+
The balance change for an asset create transaction will be as below:
|
|
75
|
+
|
|
76
|
+
```py
|
|
77
|
+
{
|
|
78
|
+
"address": "VIDHG4SYANCP2GUQXXSFSNBPJWS4TAQSI3GH4GYO54FSYPDIBYPMSF7HBY", # The asset creator
|
|
79
|
+
"asset_id": 2391, # The created asset id
|
|
80
|
+
"amount": 100000, # Full asset supply of the created asset
|
|
81
|
+
"roles": [BalanceChangeroles.AssetCreator]
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
When an asset is destroyed, the full asset supply must be in the asset creators account and the asset manager must send the destroy transaction.
|
|
86
|
+
Unfortunately we cannot determine the asset creator or full asset supply from the transaction data. As a result the balance change will always be attributed to the asset manager and will have a 0 amount.
|
|
87
|
+
If you need to account for the asset supply being destroyed from the creators account, you'll need to handle this separately.
|
|
88
|
+
|
|
89
|
+
The balance change for an asset destroy transaction will be as below:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
{
|
|
93
|
+
"address": "PIDHG4SYANCP2GUQXXSFSNBPJWS4TAQSI3GH4GYO54FSYPDIBYPMSF7HBY", # The asset destroyer, which will always be the asset manager
|
|
94
|
+
"assetId": 2391, # The destroyed asset id
|
|
95
|
+
"amount": 0, # This value will always be 0
|
|
96
|
+
"roles": [BalanceChangeroles.AssetDestroyer]
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Examples
|
|
101
|
+
|
|
102
|
+
### Data History Museum index
|
|
103
|
+
|
|
104
|
+
The following code, when algod is pointed to TestNet, will find all transactions emitted by the [Data History Museum](https://datahistory.org) since the beginning of time in _seconds_ and then find them in real-time as they emerge on the chain.
|
|
105
|
+
|
|
106
|
+
The watermark is stored in-memory so this particular example is not resilient to restarts. To change that you can implement proper persistence of the watermark. There is [an example that uses the file system](./examples/data-history-museum/) to demonstrate this.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
algorand = AlgorandClient.testnet()
|
|
110
|
+
|
|
111
|
+
# The watermark is used to track how far the subscriber has processed transactions
|
|
112
|
+
watermark = 0
|
|
113
|
+
|
|
114
|
+
def get_watermark() -> int:
|
|
115
|
+
return watermark
|
|
116
|
+
|
|
117
|
+
def set_watermark(new_watermark: int) -> None:
|
|
118
|
+
global watermark
|
|
119
|
+
watermark = new_watermark
|
|
120
|
+
|
|
121
|
+
subscriber = AlgorandSubscriber(
|
|
122
|
+
# algod is used to get the latest transactions once the subscriber has caught up to the network
|
|
123
|
+
algod_client=algorand.client.algod,
|
|
124
|
+
config={
|
|
125
|
+
"filters": [
|
|
126
|
+
{
|
|
127
|
+
"name": "dhm-asset",
|
|
128
|
+
"filter": {
|
|
129
|
+
# Match asset configuration transactions
|
|
130
|
+
"type": "acfg",
|
|
131
|
+
# Data History Museum creator account on TestNet
|
|
132
|
+
"sender": "ER7AMZRPD5KDVFWTUUVOADSOWM4RQKEEV2EDYRVSA757UHXOIEKGMBQIVU",
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
"frequency_in_seconds": 5,
|
|
137
|
+
"max_rounds_to_sync": 100,
|
|
138
|
+
"sync_behaviour": "catchup-with-indexer",
|
|
139
|
+
"watermark_persistence": {"get": get_watermark, "set": set_watermark},
|
|
140
|
+
},
|
|
141
|
+
# indexer is used to get historical transactions
|
|
142
|
+
indexer_client=algorand.client.indexer,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def process_dhm_assets(transactions: list[SubscribedTransaction], filter_name: str) -> None:
|
|
146
|
+
print(f"Received {len(transactions)} asset changes")
|
|
147
|
+
# ... do stuff with the transactions
|
|
148
|
+
|
|
149
|
+
# Attach our callback to the 'dhm-asset' filter
|
|
150
|
+
subscriber.on_batch("dhm-asset", process_dhm_assets)
|
|
151
|
+
|
|
152
|
+
def handle_error(error: Exception) -> None:
|
|
153
|
+
print(f"An error occurred: {error}")
|
|
154
|
+
|
|
155
|
+
# Attach the error handler
|
|
156
|
+
subscriber.on_error(handle_error)
|
|
157
|
+
|
|
158
|
+
# Start the subscriber
|
|
159
|
+
subscriber.start()
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### USDC real-time monitoring
|
|
163
|
+
|
|
164
|
+
The following code, when algod is pointed to MainNet, will find all transfers of [USDC](https://www.circle.com/en/usdc-multichain/algorand) that are greater than $1 and it will poll every 1s for new transfers.
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from algokit_subscriber import AlgorandSubscriber
|
|
168
|
+
from algokit_subscriber.types import SubscribedTransaction
|
|
169
|
+
from algokit_utils import AlgorandClient
|
|
170
|
+
|
|
171
|
+
algorand = AlgorandClient.mainnet()
|
|
172
|
+
|
|
173
|
+
# The watermark is used to track how far the subscriber has processed transactions
|
|
174
|
+
watermark = 0
|
|
175
|
+
|
|
176
|
+
def get_watermark() -> int:
|
|
177
|
+
return watermark
|
|
178
|
+
|
|
179
|
+
def set_watermark(new_watermark: int) -> None:
|
|
180
|
+
global watermark
|
|
181
|
+
watermark = new_watermark
|
|
182
|
+
|
|
183
|
+
subscriber = AlgorandSubscriber(
|
|
184
|
+
algod_client=algorand.client.algod,
|
|
185
|
+
config={
|
|
186
|
+
"filters": [
|
|
187
|
+
{
|
|
188
|
+
"name": "usdc",
|
|
189
|
+
"filter": {
|
|
190
|
+
"type": "axfer",
|
|
191
|
+
"asset_id": 31566704, # MainNet: USDC
|
|
192
|
+
"min_amount": 1_000_000, # $1
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
],
|
|
196
|
+
"wait_for_block_when_at_tip": True,
|
|
197
|
+
"sync_behaviour": "skip-sync-newest",
|
|
198
|
+
"watermark_persistence": {"get": get_watermark, "set": set_watermark},
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def process_usdc_transfer(transfer: SubscribedTransaction, filter_name: str) -> None:
|
|
203
|
+
asset_transfer = transfer.get("asset-transfer-transaction", {})
|
|
204
|
+
amount = asset_transfer.get("amount", 0) / 1_000_000
|
|
205
|
+
print(
|
|
206
|
+
f"{transfer['sender']} sent {asset_transfer.get('receiver')} "
|
|
207
|
+
f"USDC${amount:.2f} in transaction {transfer['id']}"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Attach our callback to the 'usdc' filter
|
|
211
|
+
subscriber.on("usdc", process_usdc_transfer)
|
|
212
|
+
|
|
213
|
+
def handle_error(error: Exception) -> None:
|
|
214
|
+
print(f"An error occurred: {error}")
|
|
215
|
+
|
|
216
|
+
# Attach the error handler
|
|
217
|
+
subscriber.on_error(handle_error)
|
|
218
|
+
|
|
219
|
+
# Start the subscriber
|
|
220
|
+
subscriber.start()
|
|
221
|
+
```
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "algokit-subscriber"
|
|
3
|
+
version = "1.0.1"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = ["Algorand Foundation"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
|
|
8
|
+
[tool.poetry.dependencies]
|
|
9
|
+
python = "^3.12"
|
|
10
|
+
py-algorand-sdk = "^2.9.1"
|
|
11
|
+
|
|
12
|
+
[tool.poetry.group.dev.dependencies]
|
|
13
|
+
algokit-utils = "^4.1.0"
|
|
14
|
+
mypy = "^1.17.1"
|
|
15
|
+
ruff = "^0.12.8"
|
|
16
|
+
pytest = "^8.4.1"
|
|
17
|
+
pre-commit = "^4.2.0"
|
|
18
|
+
black = "^25.1.0"
|
|
19
|
+
pytest-cov = "^6.2.1"
|
|
20
|
+
sphinx = "^8.2.3"
|
|
21
|
+
furo = "^2024.1.29"
|
|
22
|
+
myst-parser = "^4.0.0"
|
|
23
|
+
sphinx-autodoc2 = "^0.5.0"
|
|
24
|
+
sphinx-copybutton = "^0.5.2"
|
|
25
|
+
sphinx-autobuild = "^2024.4.16"
|
|
26
|
+
sphinx-mermaid = "^0.0.8"
|
|
27
|
+
poethepoet = "^0.36.0"
|
|
28
|
+
pytest-sugar = "^1.0.0"
|
|
29
|
+
pip-audit = "^2.9.0"
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["poetry-core"]
|
|
33
|
+
build-backend = "poetry.core.masonry.api"
|
|
34
|
+
|
|
35
|
+
[tool.ruff]
|
|
36
|
+
# TODO: eventually bring this down to 120
|
|
37
|
+
line-length = 280
|
|
38
|
+
lint.select = [
|
|
39
|
+
# all possible codes as of this ruff version are listed here,
|
|
40
|
+
# ones we don't want/need are commented out to make it clear
|
|
41
|
+
# which have been omitted on purpose vs which ones get added
|
|
42
|
+
# in new ruff releases and should be considered for enabling
|
|
43
|
+
"F", # pyflakes
|
|
44
|
+
"E",
|
|
45
|
+
"W", # pycodestyle
|
|
46
|
+
"C90", # mccabe
|
|
47
|
+
"I", # isort
|
|
48
|
+
"N", # PEP8 naming
|
|
49
|
+
"UP", # pyupgrade
|
|
50
|
+
"YTT", # flake8-2020
|
|
51
|
+
"ANN", # flake8-annotations
|
|
52
|
+
# "S", # flake8-bandit
|
|
53
|
+
# "BLE", # flake8-blind-except
|
|
54
|
+
"FBT", # flake8-boolean-trap
|
|
55
|
+
"B", # flake8-bugbear
|
|
56
|
+
"A", # flake8-builtins
|
|
57
|
+
# "COM", # flake8-commas
|
|
58
|
+
"C4", # flake8-comprehensions
|
|
59
|
+
"DTZ", # flake8-datetimez
|
|
60
|
+
"T10", # flake8-debugger
|
|
61
|
+
# "DJ", # flake8-django
|
|
62
|
+
# "EM", # flake8-errmsg
|
|
63
|
+
# "EXE", # flake8-executable
|
|
64
|
+
"ISC", # flake8-implicit-str-concat
|
|
65
|
+
"ICN", # flake8-import-conventions
|
|
66
|
+
# "G", # flake8-logging-format
|
|
67
|
+
# "INP", # flake8-no-pep420
|
|
68
|
+
"PIE", # flake8-pie
|
|
69
|
+
"T20", # flake8-print
|
|
70
|
+
"PYI", # flake8-pyi
|
|
71
|
+
"PT", # flake8-pytest-style
|
|
72
|
+
"Q", # flake8-quotes
|
|
73
|
+
"RSE", # flake8-raise
|
|
74
|
+
"RET", # flake8-return
|
|
75
|
+
"SLF", # flake8-self
|
|
76
|
+
"SIM", # flake8-simplify
|
|
77
|
+
"TID", # flake8-tidy-imports
|
|
78
|
+
"TCH", # flake8-type-checking
|
|
79
|
+
"ARG", # flake8-unused-arguments
|
|
80
|
+
"PTH", # flake8-use-pathlib
|
|
81
|
+
"ERA", # eradicate
|
|
82
|
+
# "PD", # pandas-vet
|
|
83
|
+
"PGH", # pygrep-hooks
|
|
84
|
+
"PL", # pylint
|
|
85
|
+
# "TRY", # tryceratops
|
|
86
|
+
# "NPY", # NumPy-specific rules
|
|
87
|
+
"RUF", # Ruff-specific rules
|
|
88
|
+
]
|
|
89
|
+
lint.ignore = [
|
|
90
|
+
"RET505", # allow else after return
|
|
91
|
+
"SIM108", # allow if-else in place of ternary
|
|
92
|
+
"E111", # indentation is not a multiple of four
|
|
93
|
+
"E117", # over-indented
|
|
94
|
+
"ISC001", # single line implicit string concatenation
|
|
95
|
+
"ISC002", # multi line implicit string concatenation
|
|
96
|
+
"Q000", # bad quotes inline string
|
|
97
|
+
"Q001", # bad quotes multiline string
|
|
98
|
+
"Q002", # bad quotes docstring
|
|
99
|
+
"Q003", # avoidable escaped quotes
|
|
100
|
+
"W191", # indentation contains tabs
|
|
101
|
+
"ERA001", # commented out code
|
|
102
|
+
]
|
|
103
|
+
# Exclude a variety of commonly ignored directories.
|
|
104
|
+
extend-exclude = ["docs", ".git", ".mypy_cache", ".ruff_cache"]
|
|
105
|
+
# Assume Python 3.12.
|
|
106
|
+
target-version = "py312"
|
|
107
|
+
|
|
108
|
+
[tool.ruff.lint.flake8-annotations]
|
|
109
|
+
allow-star-arg-any = true
|
|
110
|
+
suppress-none-returning = true
|
|
111
|
+
|
|
112
|
+
[tool.mypy]
|
|
113
|
+
files = ["src", "examples"]
|
|
114
|
+
exclude = ["dist", "tests"]
|
|
115
|
+
python_version = "3.12"
|
|
116
|
+
warn_unused_ignores = true
|
|
117
|
+
warn_redundant_casts = true
|
|
118
|
+
warn_unused_configs = true
|
|
119
|
+
warn_unreachable = true
|
|
120
|
+
warn_return_any = true
|
|
121
|
+
strict = true
|
|
122
|
+
disallow_untyped_decorators = true
|
|
123
|
+
disallow_any_generics = false
|
|
124
|
+
implicit_reexport = false
|
|
125
|
+
show_error_codes = true
|
|
126
|
+
|
|
127
|
+
[tool.pytest.ini_options]
|
|
128
|
+
pythonpath = ["src", "tests"]
|
|
129
|
+
|
|
130
|
+
[tool.ruff.lint.per-file-ignores]
|
|
131
|
+
"tests/*" = ["E501", "T201", "PLR2004", "F811"]
|
|
132
|
+
"examples/*" = ["T201"]
|
|
133
|
+
|
|
134
|
+
[tool.poe.tasks]
|
|
135
|
+
docs-test = { shell = "sphinx-build -b doctest docs docs/_build -W --keep-going -n -E" }
|
|
136
|
+
docs-clear = { shell = "rm -rf docs/_build" }
|
|
137
|
+
docs-build = { shell = "poetry run poe docs-clear && sphinx-build docs docs/_build -W --keep-going -n -E" }
|
|
138
|
+
docs-dev = { shell = "poetry run poe docs-build && sphinx-autobuild docs docs/_build" }
|
|
139
|
+
|
|
140
|
+
[tool.semantic_release]
|
|
141
|
+
version_toml = ["pyproject.toml:tool.poetry.version"]
|
|
142
|
+
build_command = "pip install build && python -m build"
|
|
143
|
+
commit_message = "{version}\n\n[skip ci] Automatically generated by python-semantic-release"
|
|
144
|
+
tag_format = "v{version}"
|
|
145
|
+
allow_zero_version = false
|
|
146
|
+
|
|
147
|
+
[tool.semantic_release.branches.main]
|
|
148
|
+
match = "main"
|
|
149
|
+
prerelease_token = "beta"
|
|
150
|
+
prerelease = false
|
|
151
|
+
|
|
152
|
+
[tool.semantic_release.commit_parser_options]
|
|
153
|
+
allowed_tags = [
|
|
154
|
+
"build",
|
|
155
|
+
"chore",
|
|
156
|
+
"ci",
|
|
157
|
+
"docs",
|
|
158
|
+
"feat",
|
|
159
|
+
"fix",
|
|
160
|
+
"perf",
|
|
161
|
+
"style",
|
|
162
|
+
"refactor",
|
|
163
|
+
"test",
|
|
164
|
+
]
|
|
165
|
+
minor_tags = ["feat"]
|
|
166
|
+
patch_tags = ["fix", "perf", "docs"]
|
|
167
|
+
|
|
168
|
+
[tool.semantic_release.publish]
|
|
169
|
+
dist_glob_patterns = [
|
|
170
|
+
"dist/*",
|
|
171
|
+
"stubs/dist/*",
|
|
172
|
+
] # order here is important to ensure compiler wheel is published first
|
|
173
|
+
upload_to_vcs_release = true
|
|
174
|
+
|
|
175
|
+
[tool.semantic_release.remote.token]
|
|
176
|
+
env = "GITHUB_TOKEN"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from .subscriber import AlgorandSubscriber
|
|
2
|
+
from .subscription import get_subscribed_transactions
|
|
3
|
+
from .types.arc28 import Arc28EventGroup
|
|
4
|
+
from .types.event_emitter import EventListener
|
|
5
|
+
from .types.subscription import (
|
|
6
|
+
AlgorandSubscriberConfig,
|
|
7
|
+
BalanceChange,
|
|
8
|
+
BalanceChangeRole,
|
|
9
|
+
NamedTransactionFilter,
|
|
10
|
+
SubscribedTransaction,
|
|
11
|
+
TransactionSubscriptionParams,
|
|
12
|
+
TransactionSubscriptionResult,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AlgorandSubscriber",
|
|
17
|
+
"AlgorandSubscriberConfig",
|
|
18
|
+
"Arc28EventGroup",
|
|
19
|
+
"BalanceChange",
|
|
20
|
+
"BalanceChangeRole",
|
|
21
|
+
"EventListener",
|
|
22
|
+
"NamedTransactionFilter",
|
|
23
|
+
"SubscribedTransaction",
|
|
24
|
+
"TransactionFilter",
|
|
25
|
+
"TransactionSubscriptionParams",
|
|
26
|
+
"TransactionSubscriptionResult",
|
|
27
|
+
"get_subscribed_transactions",
|
|
28
|
+
]
|