-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathairdrop.py
204 lines (169 loc) · 6.85 KB
/
airdrop.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
import time
from algosdk import mnemonic
from algosdk.constants import MICROALGOS_TO_ALGOS_RATIO
from algosdk.error import WrongChecksumError
from algosdk.future.transaction import AssetTransferTxn
from algosdk.v2client import algod, indexer
NETWORK = "testnet"
ASSET_ID = "26713649"
SENDER_ADDRESS = "5VLMDLOFA4BDSNU5QRUBISQCQJYHF5Q2HTXINUS62UNIDXWP5LJ4MHHOUY"
SENDER_PASSPHRASE = "" # 25 words separated by spaces
VALID_BLOCK_RANGE_FOR_AIRDROP = () # (start, end); leave empty for all opt-ins
MINIMUM_ALGO_HOLDING = None # leave None for global minimum of 0.1
MINIMUM_OTHER_ASA_HOLDING = 0 # leave 0 if account doesn't have to hold other ASA
ASSET_HOLDERS_INCLUDED = False # set to True if ASA holders are eligible for airdrop
SLEEP_INTERVAL = 1 # AlgoExplorer limit for public calls
AIRDROP_AMOUNT = 3000
TRANSACTION_NOTE = "Airdrop"
class NotQualified(Exception):
"""Exception for addresses not passing airdrop conditions."""
class SilentNotQualified(Exception):
"""Silent exception for addresses not passing airdrop conditions."""
## CLIENTS
def _algod_client():
"""Instantiate and return Algod client object."""
if NETWORK == "mainnet":
algod_address = "https://algoexplorerapi.io"
else:
algod_address = "https://testnet.algoexplorerapi.io"
algod_token = ""
return algod.AlgodClient(
algod_token, algod_address, headers={"User-Agent": "DoYouLoveMe?"}
)
def _indexer_client():
"""Instantiate and return Indexer client object."""
if NETWORK == "mainnet":
indexer_address = "https://algoexplorerapi.io/idx2"
else:
indexer_address = "https://testnet.algoexplorerapi.io/idx2"
indexer_token = ""
return indexer.IndexerClient(
indexer_token, indexer_address, headers={"User-Agent": "DoYouLoveMe?"}
)
## TRANSACTIONS
def _wait_for_confirmation(client, transaction_id, timeout):
"""
Wait until the transaction is confirmed or rejected, or until 'timeout'
number of rounds have passed.
Args:
transaction_id (str): the transaction to wait for
timeout (int): maximum number of rounds to wait
Returns:
dict: pending transaction information, or throws an error if the transaction
is not confirmed or rejected in the next timeout rounds
"""
start_round = client.status()["last-round"] + 1
current_round = start_round
while current_round < start_round + timeout:
try:
pending_txn = client.pending_transaction_info(transaction_id)
except Exception:
return
if pending_txn.get("confirmed-round", 0) > 0:
return pending_txn
elif pending_txn["pool-error"]:
raise Exception("pool error: {}".format(pending_txn["pool-error"]))
client.status_after_block(current_round)
current_round += 1
raise Exception(
"pending tx not found in timeout rounds, timeout value = : {}".format(timeout)
)
def check_valid_for_airdrop(item):
"""Raise an exception if provided item doesn't qualify for airdrop."""
if not ASSET_HOLDERS_INCLUDED and item.get("amount") != 0:
raise SilentNotQualified
if MINIMUM_ALGO_HOLDING is not None or MINIMUM_OTHER_ASA_HOLDING > 0:
account_info = _indexer_client().account_info(item.get("address"))
if MINIMUM_ALGO_HOLDING is not None:
if MINIMUM_ALGO_HOLDING < 0.1:
print(f"Invalid mimimum Algo holding value: {MINIMUM_ALGO_HOLDING}")
raise SystemExit
if (
account_info.get("account").get("amount") / MICROALGOS_TO_ALGOS_RATIO
< MINIMUM_ALGO_HOLDING
):
raise NotQualified
if MINIMUM_OTHER_ASA_HOLDING > 0:
if not isinstance(MINIMUM_OTHER_ASA_HOLDING, int):
print(
f"MINIMUM_OTHER_ASA_HOLDING is not an integer: {MINIMUM_OTHER_ASA_HOLDING}"
)
raise SystemExit
assets = [
asset
for asset in account_info.get("account").get("assets")
if asset.get("amount") > 0
]
if len(assets) < MINIMUM_OTHER_ASA_HOLDING:
raise NotQualified
if len(VALID_BLOCK_RANGE_FOR_AIRDROP) != 0:
if len(VALID_BLOCK_RANGE_FOR_AIRDROP) != 2:
print(f"Invalid block range: {VALID_BLOCK_RANGE_FOR_AIRDROP}")
raise SystemExit
if len(VALID_BLOCK_RANGE_FOR_AIRDROP) == 2 and (
item.get("opted-in-at-round") < VALID_BLOCK_RANGE_FOR_AIRDROP[0]
or item.get("opted-in-at-round") > VALID_BLOCK_RANGE_FOR_AIRDROP[1]
):
raise NotQualified
def address_generator():
"""Return all addresses opted-in for the asset."""
balances = _indexer_client().asset_balances(ASSET_ID)
while balances.get("balances"):
for item in balances.get("balances"):
try:
check_valid_for_airdrop(item)
yield item
except NotQualified:
print(
"Address {} is not qualified for airdrop".format(
item.get("address")
)
)
except SilentNotQualified:
pass
next_token = balances.get("next-token")
balances = _indexer_client().asset_balances(ASSET_ID, next_page=next_token)
def check_address(address):
"""Return True if address has only opt-in transaction for the asset."""
transactions = _indexer_client().search_transactions_by_address(
address, asset_id=ASSET_ID
)
return True if len(transactions.get("transactions")) == 1 else False
def send_asset(receiver):
"""Send asset to provided receiver address."""
client = _algod_client()
params = client.suggested_params()
note = TRANSACTION_NOTE
decimals = _algod_client().asset_info(ASSET_ID).get("params").get("decimals")
amount = int(AIRDROP_AMOUNT * (10 ** decimals))
unsigned_txn = AssetTransferTxn(
SENDER_ADDRESS,
params,
receiver,
amount,
index=ASSET_ID,
note=note.encode(),
)
try:
signed_txn = unsigned_txn.sign(mnemonic.to_private_key(SENDER_PASSPHRASE))
except WrongChecksumError:
return "Checksum failed to validate"
except ValueError:
return "Unknown word in passphrase"
try:
transaction_id = client.send_transaction(signed_txn)
_wait_for_confirmation(client, transaction_id, 4)
except Exception as err:
return str(err)
print(f"Amount of {AIRDROP_AMOUNT} sent to {receiver}")
return ""
if __name__ == "__main__":
for item in address_generator():
address = item.get("address")
time.sleep(SLEEP_INTERVAL)
if ASSET_HOLDERS_INCLUDED or check_address(address):
time.sleep(SLEEP_INTERVAL)
response = send_asset(address)
if response != "":
print(f"Error: {response}")
raise SystemExit