Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

index wallet balances #24

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ services:
- --db-schema=app
- --workers=4
- --batch-size=30
- --unsafe
# - --log-level=debug
# - --unfinalized-blocks=true
healthcheck:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
},
"dependencies": {
"@subql/types-cosmos": "^3.2.3",
"cross-fetch": "^4.0.0",
"@types/node": "^17.0.21",
"bech32": "^2.0.0",
"js-sha256": "^0.11.0",
Expand Down
34 changes: 32 additions & 2 deletions project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ const project: CosmosProject = {
},
network: {
// chainId: "agoriclocal",
// endpoint: ["http://host.docker.internal:26657/"],
// endpoint: ["http://agoric-subql_agd_1:26657/"],
// chainId: "agoric-emerynet-8",
// endpoint: ["https://emerynet.rpc.agoric.net:443"],
chainId: "agoric-3",
endpoint: ["https://main-a.rpc.agoric.net:443"],

Expand Down Expand Up @@ -50,6 +52,20 @@ const project: CosmosProject = {
]),
},
dataSources: [
{
kind: CosmosDatasourceKind.Runtime,
startBlock: 7179262,
endBlock: 7179262,
mapping: {
file: "./dist/index.js",
handlers: [
{
kind: CosmosHandlerKind.Block,
handler: "initiateBalancesTable",
},
],
},
},
{
kind: CosmosDatasourceKind.Runtime,
// First block of mainnet is 2115669
Expand Down Expand Up @@ -107,11 +123,25 @@ const project: CosmosProject = {
type: "state_change",
},
},
{
handler: "handleBalanceEvent",
kind: CosmosHandlerKind.Event,
filter: {
type: "coin_received",
},
},
{
handler: "handleBalanceEvent",
kind: CosmosHandlerKind.Event,
filter: {
type: "coin_spent",
},
},
],
},
},
],
};

// Must set default to the project instance
export default project;
export default project;
7 changes: 7 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -303,3 +303,10 @@ type VaultStatesDaily @entity {
liquidated: BigInt!
liquidatedClosed: BigInt!
}

type Balances @entity {
id: ID!
address: String @index
balance: BigInt
denom: String @index
}
24 changes: 24 additions & 0 deletions src/mappings/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@ export const IBC_MESSAGE_TRANSFER_VALUE = b64encode('/ibc.applications.transfer.
export const IBC_MESSAGE_RECEIVE_PACKET_VALUE = b64encode('/ibc.core.channel.v1.MsgRecvPacket');
export const RECEPIENT_KEY = b64encode('recipient');
export const SENDER_KEY = b64encode('sender');
export const SPENDER_KEY = b64encode('spender');
export const RECEIVER_KEY = b64encode('receiver');
export const AMOUNT_KEY = b64encode('amount');
export const TRANSFER_PORT_VALUE = 'transfer';

export const BALANCE_FIELDS = {
amount: 'amount',
// Bank Events
coinbase: 'minter',
coin_received: 'receiver',
coin_spent: 'spender',
transfer_recipient: 'recipient',
transfer_sender: 'sender',
burn: 'burner',
// Distribution Events
rewards: 'validator',
commission: 'validator',
proposer_reward: 'validator',
withdraw_rewards: 'validator',
withdraw_commission: 'validator',
};

export const FETCH_ACCOUNTS_URL = 'https://main-a.api.agoric.net:443/cosmos/auth/v1beta1/accounts';
export const GET_FETCH_BALANCE_URL = (address: string) =>
`https://main-a.api.agoric.net:443/cosmos/bank/v1beta1/balances/${address}`;

export const GENESIS_URL = 'https://agoric-rpc.polkachu.com/genesis';
44 changes: 44 additions & 0 deletions src/mappings/custom-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export type Balance = {
denom: string;
amount: string;
};

type PubKey = {
'@type': string;
key: string;
};

export type BaseAccount = {
'@type': string;
address: string;
pub_key: PubKey | null;
account_number: string;
sequence: string;
};

export type ModuleAccount = {
'@type': string;
base_account: BaseAccount;
name: string;
permissions: string[];
};

export type VestingAccount = {
'@type': string;
base_vesting_account: BaseAccount;
};

type Pagination = {
next_key: string | null;
total: string;
};

export type BalancesResponse = {
balances: Balance[];
pagination: Pagination;
};

export type AccountsResponse = {
accounts: (BaseAccount | ModuleAccount)[];
pagination: Pagination;
};
186 changes: 186 additions & 0 deletions src/mappings/events/balances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { Account, Balances } from '../../types';
import { BALANCE_FIELDS } from '../constants';
import { b64decode } from '../utils';
import { CosmosEvent } from '@subql/types-cosmos';
import { AMOUNT_KEY, RECEIVER_KEY, SPENDER_KEY } from '../constants';

interface Attribute {
key: string;
value: string;
}

export interface DecodedEvent {
type: string;
attributes: Attribute[];
}

export enum Operation {
Increment = 'increment',
Decrement = 'decrement',
}

interface TransactionData {
isValidTransaction: boolean;
coins: { amount: string; denom: string }[];
}

const supportedDenominations: Set<string> = new Set(['ubld', 'uist']);
export const balancesEventKit = () => {
function getAttributeValue(data: any, key: string) {
const attribute = data.attributes.find(
(attr: Attribute) => attr.key === key
);
return attribute ? attribute.value : null;
}

function getData(cosmosEvent: CosmosEvent) {
let dataAlreadyDecoded = false;
const value = getAttributeValue(cosmosEvent.event, AMOUNT_KEY);

if (!value) {
dataAlreadyDecoded = true;
}

const data = dataAlreadyDecoded
? cosmosEvent.event
: decodeEvent(cosmosEvent);

return data;
}

function decodeEvent(cosmosEvent: CosmosEvent): DecodedEvent {
const { event } = cosmosEvent;

const decodedData: DecodedEvent = {
type: event.type,
attributes: [],
};

event.attributes.forEach((attribute) => {
const decodedKey = b64decode(attribute.key);
const decodedValue = b64decode(attribute.value);

decodedData.attributes.push({
key: decodedKey,
value: decodedValue,
});
});

return decodedData;
}

async function addressExists(
address: string,
denom: string
): Promise<boolean> {
const balance = await Balances.getByFields([
['address', '=', address],
['denom', '=', denom],
]);

if (!balance || balance.length === 0) {
return false;
}
return true;
}

async function createBalancesEntry(
address: string,
denom: string,
primaryKey: string
) {
const newBalance = new Balances(primaryKey);
newBalance.address = address;
newBalance.balance = BigInt(0);
newBalance.denom = denom;
await newBalance.save();

await createAccountIfNotExists(address);

logger.info(`Created new entry for address: ${address}`);
}

async function createAccountIfNotExists(address: string): Promise<void> {
const account = await Account.get(address);
if (!account) {
const newAccount = new Account(address);
await newAccount.save();
}
}

function validateTransaction(amount: string | null): TransactionData {
const result: TransactionData = {
isValidTransaction: false,
coins: [],
};

if (!amount) {
return result;
}
const coins = amount.split(',');

for (let coin of coins) {
for (let denom of supportedDenominations) {
if (coin.endsWith(denom)) {
result.isValidTransaction = true;
result.coins.push({ amount: coin.slice(0, -denom.length), denom });
}
}
}

return result;
}

async function updateBalance(
address: string,
denom: string,
amount: bigint,
operation: Operation
): Promise<void> {
const balances = await Balances.getByFields([
['address', '=', address],
['denom', '=', denom],
]);

if (!balances || balances.length === 0) {
logger.error(`Balance not found for address: ${address}`);
return;
}

const balance = balances[0];
const currentBalance = balance.balance ?? BigInt(0);
let newBalance: bigint;

if (operation === Operation.Increment) {
newBalance = currentBalance + amount;
balance.balance = newBalance;
logger.info(
`Incremented balance for ${address} by ${amount}. New balance: ${balance.balance}`
);
} else if (operation === Operation.Decrement) {
newBalance = currentBalance - amount;
if (newBalance < 0) {
logger.error(
`Attempt to decrement ${amount} would result in a negative balance. Current balance: ${currentBalance}.`
);
return;
}
balance.balance = newBalance;
logger.info(
`Decremented balance for ${address} by ${amount}. New balance: ${balance.balance}`
);
}

await balance.save();
}

return {
validateTransaction,
getAttributeValue,
decodeEvent,
getData,
addressExists,
createBalancesEntry,
updateBalance,
};
};
Loading