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

Asset holders query #155

Merged
merged 7 commits into from
Apr 23, 2019
Merged
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
2 changes: 2 additions & 0 deletions src/model/asset_id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// String like USD-GBSTRUSD7IRX73RQZBL3RQUH6KS3O4NYFY3QCALDLZD77XMZOPWAVTUK
export type AssetID = string;
17 changes: 13 additions & 4 deletions src/model/balance.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Asset } from "stellar-sdk";
import { toFloatAmountString } from "../util/stellar";
import { AccountID } from "./account_id";
import { AccountID, AssetID } from "./";

export interface IBalanceBase {
account: AccountID;
Expand All @@ -15,6 +14,12 @@ export interface IBalance extends IBalanceBase {
}

export class Balance implements IBalance {
public static parsePagingToken(token: string): [AccountID, AssetID, string] {
return Buffer.from(token, "base64")
.toString()
.split("_") as [AccountID, AssetID, string];
}

public account: AccountID;
public asset: Asset;
public limit: string;
Expand All @@ -24,10 +29,14 @@ export class Balance implements IBalance {

constructor(data: IBalance) {
this.account = data.account;
this.limit = toFloatAmountString(data.limit);
this.balance = toFloatAmountString(data.balance);
this.limit = data.limit;
this.balance = data.balance;
this.lastModified = data.lastModified;
this.authorized = data.authorized;
this.asset = data.asset;
}

public get paging_token() {
return Buffer.from(`${this.account}_${this.asset.toString()}_${this.balance}`).toString("base64");
}
}
5 changes: 2 additions & 3 deletions src/model/balance_values.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Asset } from "stellar-sdk";
import { toFloatAmountString } from "../util/stellar";
import { IBalanceBase } from "./balance";

export class BalanceValues implements IBalanceBase {
Expand All @@ -11,8 +10,8 @@ export class BalanceValues implements IBalanceBase {

constructor(data: IBalanceBase) {
this.account = data.account;
this.limit = toFloatAmountString(data.limit);
this.balance = toFloatAmountString(data.balance);
this.limit = data.limit;
this.balance = data.balance;
this.authorized = data.authorized;
this.asset = data.asset;
}
Expand Down
1 change: 1 addition & 0 deletions src/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from "./account_thresholds";
export * from "./account_flags";
export * from "./account_values";
export * from "./account_subscription_payload";
export * from "./asset_id";
export * from "./asset_input";
export * from "./data_entry";
export * from "./data_entry_subscription_payload";
Expand Down
33 changes: 32 additions & 1 deletion src/repo/assets.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { IDatabase } from "pg-promise";
import squel from "squel";
import { Asset } from "stellar-base";
import { Asset } from "stellar-sdk";
import { PagingParams } from "../datasource/horizon/base";
import { Balance } from "../model";
import { BalanceFactory } from "../model/factories";
import { parseCursorPagination, properlyOrdered, SortOrder } from "../util/paging";

export default class AssetsRepo {
private db: IDatabase<any>;
Expand Down Expand Up @@ -39,4 +43,31 @@ export default class AssetsRepo {

return res.map(a => new Asset(a.assetcode, a.issuer));
}

public async findHolders(asset: Asset, paging: PagingParams) {
const { limit, cursor, order } = parseCursorPagination(paging);
const queryBuilder = squel
.select()
.from("trustlines")
.where("assetcode = ?", asset.getCode())
.where("issuer = ?", asset.getIssuer())
.order("balance", order === SortOrder.ASC) // we must order by balance first to support cursor pagination
.order("accountid", order !== SortOrder.ASC) // inversion of above
.limit(limit);

if (cursor) {
const [accountId, , balance] = Balance.parsePagingToken(cursor);

if (paging.after) {
queryBuilder.where("(balance = ? AND accountid > ?) OR balance < ?", balance, accountId, balance);
} else if (paging.before) {
queryBuilder.where("(balance = ? AND accountid < ?) OR balance > ?", balance, accountId, balance);
}
}

const res = await this.db.manyOrNone(queryBuilder.toString());
const balances = res.map(r => BalanceFactory.fromDb(r));

return properlyOrdered(balances, paging);
}
}
6 changes: 6 additions & 0 deletions src/schema/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export const typeDefs = gql`
issuer: Account
"Asset's code"
code: AssetCode!
"All accounts that trust this asset, ordered by balance"
balances(first: Int, last: Int, after: String, before: String): BalanceConnection
}

"Represents single [asset](https://www.stellar.org/developers/guides/concepts/assets.html) on Stellar network with additional statistics, provided by Horizon"
Expand All @@ -30,6 +32,8 @@ export const typeDefs = gql`
numAccounts: Int
"Asset's issuer account flags"
flags: AccountFlags
"All accounts that trust this asset, ordered by balance"
balances(first: Int, last: Int, after: String, before: String): BalanceConnection
}

"A list of assets"
Expand All @@ -50,6 +54,8 @@ export const typeDefs = gql`
}

type Query {
"Get single asset"
asset(id: AssetID): Asset
"Get list of assets"
assets(
code: AssetCode
Expand Down
17 changes: 15 additions & 2 deletions src/schema/resolvers/asset.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import { Asset } from "stellar-sdk";
import { db } from "../../database";
import { IHorizonAssetData } from "../../datasource/types";
import { IApolloContext } from "../../graphql_server";
import { AssetID } from "../../model";
import { AssetFactory } from "../../model/factories";
import * as resolvers from "./shared";
import { makeConnection } from "./util";

const holdersResolver = async (root: Asset, args: any, ctx: IApolloContext, info: any) => {
const balances = await db.assets.findHolders(root, args);

return makeConnection(balances);
};

export default {
Asset: { issuer: resolvers.account },
AssetWithInfo: { issuer: resolvers.account },
Asset: { issuer: resolvers.account, balances: holdersResolver },
AssetWithInfo: { issuer: resolvers.account, balances: holdersResolver },
Query: {
asset: async (root: any, args: { id: AssetID }, ctx: IApolloContext, info: any) => {
return AssetFactory.fromId(args.id);
},
assets: async (root: any, args: any, ctx: IApolloContext, info: any) => {
const { code, issuer } = args;
const records: IHorizonAssetData[] = await ctx.dataSources.assets.all(
Expand Down
11 changes: 8 additions & 3 deletions src/schema/resolvers/balance.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { withFilter } from "graphql-subscriptions";
import { IApolloContext } from "../../graphql_server";
import { BalanceSubscriptionPayload } from "../../model";
import { Balance, BalanceSubscriptionPayload } from "../../model";
import { BALANCE, pubsub } from "../../pubsub";
import { toFloatAmountString } from "../../util/stellar";
import * as resolvers from "./shared";
import { eventMatches } from "./util";

Expand All @@ -24,15 +25,19 @@ export default {
Balance: {
account: resolvers.account,
ledger: resolvers.ledger,
asset: resolvers.asset
asset: resolvers.asset,
limit: (root: Balance) => toFloatAmountString(root.limit),
balance: (root: Balance) => toFloatAmountString(root.balance)
},
BalanceSubscriptionPayload: {
account: resolvers.account,
asset: resolvers.asset
},
BalanceValues: {
account: resolvers.account,
asset: resolvers.asset
asset: resolvers.asset,
limit: (root: Balance) => toFloatAmountString(root.limit),
balance: (root: Balance) => toFloatAmountString(root.balance)
},
Subscription: { balance: balanceSubscription(BALANCE) }
};
4 changes: 2 additions & 2 deletions src/schema/resolvers/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ export function idOnlyRequested(info: any): boolean {
return false;
}

export function makeConnection<T extends IWithPagingToken, R>(records: T[], nodeBuilder: (r: T) => R) {
export function makeConnection<T extends IWithPagingToken, R>(records: T[], nodeBuilder?: (r: T) => R) {
const edges = records.map(record => {
return {
node: nodeBuilder(record),
node: nodeBuilder ? nodeBuilder(record) : record,
cursor: record.paging_token
};
});
Expand Down
11 changes: 11 additions & 0 deletions src/schema/type_defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ export const typeDefs = gql`
ledger: Ledger!
}

type BalanceConnection {
pageInfo: PageInfo!
nodes: [Balance]
edges: [BalanceEdge]
}

type BalanceEdge {
cursor: String!
node: Balance
}

"Represents a current [trustline](https://www.stellar.org/developers/guides/concepts/assets.html#trustlines) state, which is broadcasting to subscribers"
type BalanceValues implements IBalance {
account: Account
Expand Down
20 changes: 20 additions & 0 deletions src/util/paging.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { PagingParams } from "../datasource/horizon/base";

export enum SortOrder {
DESC = "desc",
ASC = "asc"
Expand All @@ -10,3 +12,21 @@ export function invertSortOrder(order: SortOrder) {

return SortOrder.DESC;
}

export function parseCursorPagination(args: PagingParams) {
const { first, after, last, before, order = SortOrder.DESC } = args;

if (!first && !last) {
throw new Error("Missing paging parameters");
}

return {
limit: first || last,
order: last ? invertSortOrder(order) : order,
cursor: last ? before : after
};
}

export function properlyOrdered(records: any[], pagingParams: PagingParams): any[] {
return pagingParams.last ? records.reverse() : records;
}
9 changes: 4 additions & 5 deletions tests/unit/model/balance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Asset } from "stellar-sdk";
import { Balance } from "../../../src/model";
import { BalanceFactory } from "../../../src/model/factories";
import { MAX_INT64 } from "../../../src/util";
import { toFloatAmountString } from "../../../src/util/stellar";
import AccountFactory from "../../factories/account";

const data = {
Expand All @@ -25,8 +24,8 @@ describe("constructor", () => {

it("sets account id", () => expect(subject.account).toEqual(data.accountid));
it("sets lastModified", () => expect(subject.lastModified).toEqual(data.lastmodified));
it("formats limit", () => expect(subject.limit).toEqual("922337203685.4775807"));
it("formats balance", () => expect(subject.balance).toEqual("960.0000000"));
it("sets limit", () => expect(subject.limit).toEqual("9223372036854775807"));
it("sets balance", () => expect(subject.balance).toEqual("9600000000"));
it("sets authorized", () => expect(subject.authorized).toBe(true));
it("sets asset", () => {
expect(subject.asset).toBeInstanceOf(Asset);
Expand All @@ -42,8 +41,8 @@ describe("static buildFakeNative(account)", () => {

expect(fake).toMatchObject({
account: account.id,
balance: toFloatAmountString(account.balance),
limit: toFloatAmountString(MAX_INT64),
balance: account.balance,
limit: MAX_INT64,
authorized: true,
lastModified: account.lastModified
});
Expand Down