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 1 commit
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;
9 changes: 6 additions & 3 deletions src/model/balance.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 { AccountID } from "./account_id";

export interface IBalanceBase {
Expand All @@ -24,10 +23,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
39 changes: 38 additions & 1 deletion src/repo/assets.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
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 { 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 +42,38 @@ export default class AssetsRepo {

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

public async findHolders(asset: Asset, paging: PagingParams) {
const queryBuilder = squel
.select()
.from("trustlines")
.where("assetcode = ?", asset.getCode())
.where("issuer = ?", asset.getIssuer());

const { limit, cursor, order } = parseCursorPagination(paging);

queryBuilder.limit(limit);

// Order of these stetements is important,
// we must order by balance first to support cursor pagination
queryBuilder.order("balance", order === SortOrder.ASC);
queryBuilder.order("accountid");

if (cursor) {
const [, , balance] = Buffer.from(cursor, "base64")
.toString()
.split("_");

if (paging.after) {
queryBuilder.where("balance < ?", balance);
} else {
queryBuilder.where("balance > ?", 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"
holders(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"
holders(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, holders: holdersResolver },
AssetWithInfo: { issuer: resolvers.account, holders: 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
24 changes: 24 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,25 @@ 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: before ? invertSortOrder(order) : order,
cursor: last ? before : after
};
}

export function properlyOrdered(records: any[], pagingParams: PagingParams): any[] {
if (pagingParams.last && pagingParams.before) {
return records.reverse();
}

return 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