Skip to content

Commit

Permalink
Asset holders query
Browse files Browse the repository at this point in the history
  • Loading branch information
charlie-wasp authored Apr 23, 2019
1 parent 66292d5 commit 558b8b3
Show file tree
Hide file tree
Showing 12 changed files with 116 additions and 20 deletions.
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

0 comments on commit 558b8b3

Please sign in to comment.