def allowances(): # pylint: disable = W "Show the amount of each asset that you could sell tax-free" buchfink_db = BuchfinkDB() num_matched_accounts = 0 all_trades = [] for account in buchfink_db.get_all_accounts(): num_matched_accounts += 1 all_trades.extend(buchfink_db.get_local_trades_for_account(account.name)) logger.info('Collected %d trades from %d exchange account(s)', len(all_trades), num_matched_accounts) accountant = buchfink_db.get_accountant() currency = buchfink_db.get_main_currency() currency_in_usd = FVal(buchfink_db.inquirer.find_usd_price(currency)) accountant.process_history(epoch_start_ts, epoch_end_ts, all_trades, [], [], [], [], []) total_usd = FVal(0) table = [] raise NotImplementedError() """
def quote(asset: Tuple[str], amount: float, base_asset_: Optional[str], timestamp: Optional[str]): buchfink_db = BuchfinkDB() buchfink_db.perform_assets_updates() base_asset = buchfink_db.get_asset_by_symbol(base_asset_) \ if base_asset_ \ else buchfink_db.get_main_currency() base_in_usd = FVal(buchfink_db.inquirer.find_usd_price(base_asset)) a_usd = buchfink_db.get_asset_by_symbol('USD') ds_timestamp = deserialize_timestamp(timestamp) if timestamp else None historian = PriceHistorian() for symbol in asset: asset_ = buchfink_db.get_asset_by_symbol(symbol) if ds_timestamp: asset_usd = historian.query_historical_price( from_asset=asset_, to_asset=a_usd, timestamp=ds_timestamp ) else: asset_usd = FVal(buchfink_db.inquirer.find_usd_price(asset_)) click.echo('{} {} = {} {}'.format( click.style(f'{amount}', fg='white'), click.style(asset_.symbol, fg='green'), click.style(f'{FVal(amount) * asset_usd / base_in_usd}', fg='white'), click.style(base_asset.symbol, fg='green') ))
def serialize_ledger_action(action: LedgerAction): ser_action = action.serialize() ser_action['timestamp'] = serialize_timestamp(action.timestamp) if action.action_type == LedgerActionType.AIRDROP: ser_action['airdrop'] = serialize_amount(FVal(action.amount), action.asset) del ser_action['asset'] del ser_action['amount'] del ser_action['action_type'] elif action.action_type == LedgerActionType.INCOME: ser_action['income'] = serialize_amount(FVal(action.amount), action.asset) del ser_action['asset'] del ser_action['amount'] del ser_action['action_type'] elif action.action_type == LedgerActionType.GIFT: ser_action['gift'] = serialize_amount(FVal(action.amount), action.asset) del ser_action['asset'] del ser_action['amount'] del ser_action['action_type'] elif action.action_type == LedgerActionType.LOSS: ser_action['loss'] = serialize_amount(FVal(action.amount), action.asset) del ser_action['asset'] del ser_action['amount'] del ser_action['action_type'] elif action.action_type == LedgerActionType.EXPENSE: ser_action['expense'] = serialize_amount(FVal(action.amount), action.asset) del ser_action['asset'] del ser_action['amount'] del ser_action['action_type'] if not ser_action['identifier']: del ser_action['identifier'] if not ser_action['location']: del ser_action['location'] if not ser_action['notes']: del ser_action['notes'] if not ser_action['link']: del ser_action['link'] if not ser_action['rate']: del ser_action['rate'] if not ser_action['rate_asset']: del ser_action['rate_asset'] return ser_action
def deserialize_trade(trade_dict) -> Trade: if 'pair' in trade_dict: return Trade(trade_dict['timestamp'], trade_dict.get('location', ''), trade_dict['pair'], deserialize_tradetype(trade_dict['trade_type']), deserialize_fval(trade_dict['amount']), deserialize_fval(trade_dict['rate']), deserialize_fval(trade_dict['fee']), deserialize_asset(trade_dict['fee_currency']), trade_dict['link']) else: if 'buy' in trade_dict: trade_type = TradeType.BUY amount, base_asset = deserialize_amount(trade_dict['buy']) elif 'sell' in trade_dict: trade_type = TradeType.SELL amount, base_asset = deserialize_amount(trade_dict['sell']) else: raise ValueError('Invalid trade: ' + str(trade_dict)) quote_amount, quote_asset = deserialize_amount(trade_dict['for']) if 'fee' in trade_dict: fee, fee_currency = deserialize_amount(trade_dict['fee']) else: fee, fee_currency = FVal('0'), quote_asset return Trade( dateutil.parser.isoparse(trade_dict['timestamp']).timestamp(), trade_dict.get('location', ''), '{0}_{1}'.format(base_asset.symbol, quote_asset.symbol), trade_type, amount, quote_amount / amount, fee, fee_currency, trade_dict.get('link', ''))
def test_ethereum_balances(): start_ts = datetime.fromisoformat('2015-01-01').timestamp() end_ts = datetime.fromisoformat('2019-01-01').timestamp() buchfink_db = BuchfinkDB( os.path.join(os.path.dirname(__file__), 'scenarios', 'ethereum')) whale = buchfink_db.get_all_accounts()[0] sheet = buchfink_db.query_balances(whale) assert sheet.assets['ETH'].amount == FVal('147699.424503407102942053')
def balances(keyword): "Show balances across all accounts" buchfink_db = BuchfinkDB() balances_sum = {} usd_value_sum = {} for account in buchfink_db.get_all_accounts(): if keyword is not None and keyword not in account['name']: continue if 'exchange' in account: exchange = buchfink_db.get_exchange(account['name']) api_key_is_valid, error = exchange.validate_api_key() if not api_key_is_valid: logger.critical( 'Skipping exchange %s because API key is not valid (%s)', account['name'], error) continue balances, error = exchange.query_balances() if not error: logger.info('Fetched balances for %d assets from %s', len(balances.keys()), account['name']) for asset, balance in balances.items(): amount = balance['amount'] balances_sum[asset] = balances_sum.get(asset, FVal(0)) + amount if 'usd_value' in balance: usd_value_sum[asset] = usd_value_sum.get( asset, FVal(0)) + balance['usd_value'] elif 'ethereum' in account: manager = buchfink_db.get_chain_manager(account) manager.query_balances() for eth_balance in manager.balances.eth.values(): for asset, balance in eth_balance.asset_balances.items(): amount = balance.amount balances_sum[asset] = balances_sum.get(asset, FVal(0)) + amount usd_value_sum[asset] = usd_value_sum.get( asset, FVal(0)) + balance.usd_value elif 'bitcoin' in account: manager = buchfink_db.get_chain_manager(account) manager.query_balances() asset = Asset('BTC') for balance in manager.balances.btc.values(): amount = balance.amount balances_sum[asset] = balances_sum.get(asset, FVal(0)) + amount usd_value_sum[asset] = usd_value_sum.get( asset, FVal(0)) + balance.usd_value elif 'file' in account: account = yaml.load(open(account['file'], 'r'), Loader=yaml.SafeLoader) if 'balances' in account: for balance in account['balances']: amount = FVal(balance['amount']) asset = Asset(balance['asset']) usd_value = amount * buchfink_db.inquirer.find_usd_price( asset) balances_sum[asset] = balances_sum.get(asset, FVal(0)) + amount usd_value_sum[asset] = usd_value_sum.get( asset, FVal(0)) + usd_value currency = buchfink_db.get_main_currency() currency_in_usd = buchfink_db.inquirer.find_usd_price(currency) table = [] assets = [ obj[0] for obj in sorted( usd_value_sum.items(), key=itemgetter(1), reverse=True) ] balance_in_currency_sum = 0 for asset in assets: balance = balances_sum[asset] balance_in_currency = usd_value_sum.get(asset, FVal(0)) / currency_in_usd balance_in_currency_sum += balance_in_currency table.append([ asset, balance, asset.symbol, round(float(balance_in_currency), 2) ]) table.append( ['Total', None, None, round(float(balance_in_currency_sum), 2)]) print( tabulate(table, headers=[ 'Asset', 'Amount', 'Symbol', 'Fiat Value (%s)' % currency.symbol ]))
def deserialize_fval(val: str) -> FVal: return FVal(val)
def deserialize_amount(amount: str) -> Tuple[FVal, Optional[Asset]]: elems = amount.split(' ') amount = FVal(elems[0]) asset = deserialize_asset(elems[1]) if len(elems) > 1 else None return amount, asset
def deserialize_balance(balance: Dict[str, Any], buchfink_db) -> Tuple[Balance, Asset]: amount = FVal(balance['amount']) asset = buchfink_db.get_asset_by_symbol(balance['asset']) usd_value = amount * FVal(buchfink_db.inquirer.find_usd_price(asset)) return Balance(amount, usd_value), asset
def balances(keyword, minimum_balance, fetch, total, external): "Show balances across all accounts" buchfink_db = BuchfinkDB() assets_sum = {} assets_usd_sum = {} liabilities_sum = {} liabilities_usd_sum = {} if external: accounts = [account_from_string(ext, buchfink_db) for ext in external] else: accounts = buchfink_db.get_all_accounts() for account in accounts: if keyword is not None and keyword not in account.name: continue if fetch: buchfink_db.perform_assets_updates() buchfink_db.fetch_balances(account) sheet = buchfink_db.get_balances(account) for asset, balance in sheet.assets.items(): amount = balance.amount assets_sum[asset] = assets_sum.get(asset, FVal(0)) + amount assets_usd_sum[asset] = assets_usd_sum.get(asset, FVal(0)) + balance.usd_value for liability, balance in sheet.liabilities.items(): amount = balance.amount liabilities_sum[liability] = liabilities_sum.get(liability, FVal(0)) + amount liabilities_usd_sum[liability] = liabilities_usd_sum.get(liability, FVal(0)) \ + balance.usd_value currency = buchfink_db.get_main_currency() currency_in_usd = FVal(buchfink_db.inquirer.find_usd_price(currency)) table = [] assets = [obj[0] for obj in sorted(assets_usd_sum.items(), key=itemgetter(1), reverse=True)] balance_in_currency_sum = 0 small_balances_sum = 0 for asset in assets: balance = assets_sum[asset] balance_in_currency = FVal(assets_usd_sum.get(asset, 0)) / currency_in_usd if balance > ZERO: if balance_in_currency > FVal(minimum_balance): table.append([ asset.name, balance, asset.symbol, round(float(balance_in_currency), 2) ]) else: small_balances_sum += balance_in_currency balance_in_currency_sum += balance_in_currency if total: print(f'Total assets: {round(float(balance_in_currency_sum), 2)} {currency.symbol}') else: if small_balances_sum > 0: table.append(['Others', None, None, round(float(small_balances_sum), 2)]) table.append(['Total', None, None, round(float(balance_in_currency_sum), 2)]) print(tabulate(table, headers=[ 'Asset', 'Amount', 'Symbol', 'Fiat Value (%s)' % currency.symbol ])) if liabilities_sum: table = [] balance_in_currency_sum = 0 assets = [ obj[0] for obj in sorted(liabilities_usd_sum.items(), key=itemgetter(1), reverse=True) ] for asset in assets: balance = liabilities_sum[asset] balance_in_currency = liabilities_usd_sum.get(asset, FVal(0)) / currency_in_usd if balance > ZERO and balance_in_currency >= FVal(minimum_balance): balance_in_currency_sum += balance_in_currency table.append([ asset.name, balance, asset.symbol, round(float(balance_in_currency), 2) ]) table.append(['Total', None, None, round(float(balance_in_currency_sum), 2)]) if total: print( f'Total liabilities: ' f'{round(float(balance_in_currency_sum), 2)} {currency.symbol}' ) else: print() print(tabulate(table, headers=[ 'Liability', 'Amount', 'Symbol', 'Fiat Value (%s)' % currency.symbol ]))
def dummy_trade(): return Trade( datetime(2020, 1, 3, tzinfo=timezone.utc).timestamp(), 'coinbase', 'BTC_EUR', TradeType.BUY, FVal('0.52'), FVal('7200.0'), FVal('0.5'), Asset('EUR'), 'LINK-123')
def classify_tx(account: Account, tx_hash: str, txn: EthereumTransaction, receipt: dict) \ -> List[LedgerAction]: actions = [] # type: List[LedgerAction] tx_time = serialize_timestamp(txn.timestamp) if txn.from_address != account.address: return actions for event in receipt['logs']: if event['topics'][0] == CLAIMED and event[ 'address'] == ADDR_UNISWAP_AIRDROP.lower(): amount = hexstr_to_int(event['data'][130:]) actions += [ LedgerAction( identifier=None, location='', action_type=LedgerActionType.AIRDROP, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token( '_ceth_0x1f9840a85d5af5bf1d1762f925bdaddc4201f984'), notes='', link=tx_hash) ] elif event['topics'][0] == CLAIMED and event[ 'address'] == ADDR_MIRROR_AIRDROP.lower(): amount = hexstr_to_int(event['data'][130:]) actions += [ LedgerAction( identifier=None, location='', action_type=LedgerActionType.AIRDROP, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token( '_ceth_0x09a3ecafa817268f77be1283176b946c4ff2e608'), notes='', link=tx_hash) ] elif event['topics'][0] == CLAIMED and event[ 'address'] == ADDR_POOL_AIRDROP.lower(): amount = hexstr_to_int(event['data'][130:]) actions += [ LedgerAction( identifier=None, location='', action_type=LedgerActionType.AIRDROP, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token( '_ceth_0x0cec1a9154ff802e7934fc916ed7ca50bde6844e'), notes='PoolTogether airdrop', link=tx_hash) ] elif event['topics'][0] == CLAIMED and event[ 'address'] == ADDR_IMX_AIRDROP.lower(): amount = hexstr_to_int(event['data'][130:]) actions += [ LedgerAction( identifier=None, location='', action_type=LedgerActionType.AIRDROP, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token( '_ceth_0x7b35ce522cb72e4077baeb96cb923a5529764a00'), notes='IMX airdrop', link=tx_hash) ] elif event['topics'][0] == CLAIMED: logger.warning('Unknown Claimed event for tx %s at %s', tx_hash, tx_time) if event['topics'][0] == CLAIMED_3 and event[ 'address'] == ADDR_BADGER_TREE.lower(): if hexstr_to_int(event['topics'][2]) == hexstr_to_int(ADDR_BADGER): amount = hexstr_to_int(event['data'][2:66]) token = symbol_to_asset_or_token( '_ceth_0x3472a5a71965499acd81997a54bba8d852c6e53d') actions += [ LedgerAction(identifier=None, location='', action_type=LedgerActionType.INCOME, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=token, notes='Badger rewards for staking', link=tx_hash) ] elif event['topics'][0] == CLAIMED_3: logger.warning('Unknown Claimed event for tx %s at %s', tx_hash, tx_time) if event['topics'][0] == CLAIMED_2 and event[ 'address'] == ADDR_XTOKEN_AIRDROP.lower(): amount = hexstr_to_int(event['data']) actions += [ LedgerAction( identifier=None, location='', action_type=LedgerActionType.AIRDROP, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token( '_ceth_0x7f3edcdd180dbe4819bd98fee8929b5cedb3adeb'), notes='xToken airdrop', link=tx_hash) ] elif event['topics'][0] == CLAIMED_2 and event[ 'address'] == ADDR_BALANCER_REWARDS.lower(): amount = hexstr_to_int(event['data'][66:]) actions += [ LedgerAction(identifier=None, location='', action_type=LedgerActionType.INCOME, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token('BAL'), notes='Balancer rewards for providing liquidity', link=tx_hash) ] elif event['topics'][0] == CLAIMED_2 and event[ 'address'] == ADDR_ROOK_REWARDS.lower(): amount = hexstr_to_int(event['data'][2:]) actions += [ LedgerAction(identifier=None, location='', action_type=LedgerActionType.INCOME, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token('ROOK'), notes='Rook rewards for providing liquidity', link=tx_hash) ] elif event['topics'][0] == CLAIMED_2: logger.warning('Unknown Claimed event for tx: %s', tx_hash) if event['topics'][0] == CLAIMED_4 and event[ 'address'] == ADDR_GITCOIN_AIRDROP.lower(): amount = hexstr_to_int(event['data'][2:][128:192]) actions += [ LedgerAction( identifier=None, location='', action_type=LedgerActionType.INCOME, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token( '_ceth_0xDe30da39c46104798bB5aA3fe8B9e0e1F348163F'), notes='Gitcoin retroactive airdrop', link=tx_hash) ] elif event['topics'][0] == CLAIMED_4: logger.warning('Unknown Claimed event for tx: %s', tx_hash) if event['topics'][0] == REWARD_PAID and event[ 'address'] in ADDR_PIEDAO_INCENTIVES: amount = hexstr_to_int(event['data'][2:]) actions += [ LedgerAction(identifier=None, location='', action_type=LedgerActionType.INCOME, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token('DOUGH'), notes='rewards for providing liquidity', link=tx_hash) ] elif event['topics'][0] == REWARD_PAID and event[ 'address'] == ADDR_INDEX_REWARDS.lower(): amount = hexstr_to_int(event['data'][2:]) actions += [ LedgerAction(identifier=None, location='', action_type=LedgerActionType.INCOME, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token('INDEX'), notes='rewards for providing liquidity', link=tx_hash) ] elif event['topics'][0] == REWARD_PAID and event[ 'address'] == ADDR_YFI_GOVERNANCE.lower(): amount = hexstr_to_int(event['data'][2:]) actions += [ LedgerAction( identifier=None, location='', action_type=LedgerActionType.INCOME, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token('yDAI+yUSDC+yUSDT+yTUSD'), notes='rewards from yearn governance', link=tx_hash) ] elif event['topics'][0] == REWARD_PAID and event[ 'address'] in ADDR_CREAM_REWARDS: amount = hexstr_to_int(event['data'][2:]) actions += [ LedgerAction(identifier=None, location='', action_type=LedgerActionType.INCOME, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token('CREAM'), notes='rewards from cream incentives', link=tx_hash) ] elif event['topics'][0] == REWARD_PAID and event[ 'address'] == ADDR_MIR_REWARDS.lower(): amount = hexstr_to_int(event['data'][2:]) actions += [ LedgerAction( identifier=None, location='', action_type=LedgerActionType.INCOME, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token( '_ceth_0x09a3ecafa817268f77be1283176b946c4ff2e608'), notes='rewards for staking MIR LP', link=tx_hash) ] elif event['topics'][0] == REWARD_PAID: logger.warning('Unknown RewardPaid event for tx %s at %s', tx_hash, tx_time) if event['topics'][0] == MINTED and event[ 'address'] == ADDR_SWERVE_MINTER.lower(): if hexstr_to_int(event['topics'][1]) == hexstr_to_int( account.address): amount = hexstr_to_int(event['data'][66:]) actions += [ LedgerAction(identifier=None, location='', action_type=LedgerActionType.INCOME, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token('SWRV'), notes='Swerve rewards for pooling liquidity', link=tx_hash) ] elif event['topics'][0] == MINTED: logger.warning('Unknown Minted event for tx %s at %s', tx_hash, tx_time) if event['topics'][0] == PURCHASE and event[ 'address'] == ADDR_FEI_GENESIS_GROUP.lower(): if hexstr_to_int(event['topics'][1]) == hexstr_to_int( account.address): amount = hexstr_to_int(event['data']) actions += [ LedgerAction(identifier=None, location='', action_type=LedgerActionType.EXPENSE, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token('ETH'), notes='Fei Genesis Commit', link=tx_hash) ] elif event['topics'][0] == PURCHASE: logger.warning('Unknown Purchase event for tx %s at %s', tx_hash, tx_time) if event['topics'][0] == TRANSFER and event[ 'address'] == ADDR_DODO.lower(): if hexstr_to_int(event['topics'][1]) == hexstr_to_int(ADDR_DODO_REWARDS) and \ hexstr_to_int(event['topics'][2]) == hexstr_to_int(account.address): amount = hexstr_to_int(event['data']) actions += [ LedgerAction(identifier=None, location='', action_type=LedgerActionType.INCOME, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token('DODO'), notes='Claim DODO rewards', link=tx_hash) ] elif event['topics'][0] == TRANSFER and event[ 'address'] == ADDR_SUSHI.lower(): if hexstr_to_int(event['topics'][1]) == hexstr_to_int(ADDR_SUSHI_REWARDS) and \ hexstr_to_int(event['topics'][2]) == hexstr_to_int(account.address): amount = hexstr_to_int(event['data']) actions += [ LedgerAction(identifier=None, location='', action_type=LedgerActionType.INCOME, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token('SUSHI'), notes='Claim SUSHI rewards for staking LP', link=tx_hash) ] elif event['topics'][0] == TRANSFER and event[ 'address'] == ADDR_TORN.lower(): if hexstr_to_int(event['topics'][1]) == hexstr_to_int(ADDR_TORN_VTORN) and \ hexstr_to_int(event['topics'][2]) == hexstr_to_int(account.address): amount = hexstr_to_int(event['data']) actions += [ LedgerAction( identifier=None, location='', action_type=LedgerActionType.AIRDROP, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token( '_ceth_0x77777FeDdddFfC19Ff86DB637967013e6C6A116C' ), notes='TORN airdrop', link=tx_hash) ] elif event['topics'][0] == STAKEEND and event[ 'address'] == ADDR_HEX.lower(): if hexstr_to_int(event['topics'][1]) == hexstr_to_int( account.address): payout = hexstr_to_int(event['data'][2:][:18]) actions += [ LedgerAction(identifier=None, location='', action_type=LedgerActionType.INCOME, amount=FVal(payout) / FVal(1e8), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token('_ceth_' + ADDR_HEX), notes='HEX Payout for staking', link=tx_hash) ] if event['topics'][0] == HUNT and event[ 'address'] == ADDR_BLACKPOOL_AIRDROP.lower(): if hexstr_to_int(event['topics'][1]) == hexstr_to_int( account.address): amount = hexstr_to_int(event['data'][2:][64:128]) actions += [ LedgerAction( identifier=None, location='', action_type=LedgerActionType.AIRDROP, amount=FVal(amount) / FVal(1e18), rate=None, rate_asset=None, timestamp=txn.timestamp, asset=symbol_to_asset_or_token( '_ceth_0x0eC9F76202a7061eB9b3a7D6B59D36215A7e37da' ), notes='Blackpool airdrop', link=tx_hash) ] return actions