From fa92ea9acfe8c0f9786978c7763a1577bf111787 Mon Sep 17 00:00:00 2001 From: Timur Ramazanov Date: Mon, 15 Apr 2019 15:19:16 +0300 Subject: [PATCH] Asset holders query --- src/model/asset_id.ts | 2 ++ src/model/balance.ts | 9 +++++--- src/model/balance_values.ts | 5 ++-- src/model/index.ts | 1 + src/repo/assets.ts | 39 +++++++++++++++++++++++++++++++- src/schema/assets.ts | 6 +++++ src/schema/resolvers/asset.ts | 17 ++++++++++++-- src/schema/resolvers/balance.ts | 11 ++++++--- src/schema/resolvers/util.ts | 4 ++-- src/schema/type_defs.ts | 11 +++++++++ src/util/paging.ts | 24 ++++++++++++++++++++ tests/unit/model/balance.test.ts | 4 ++-- 12 files changed, 117 insertions(+), 16 deletions(-) create mode 100644 src/model/asset_id.ts diff --git a/src/model/asset_id.ts b/src/model/asset_id.ts new file mode 100644 index 00000000..5c7860ce --- /dev/null +++ b/src/model/asset_id.ts @@ -0,0 +1,2 @@ +// String like USD-GBSTRUSD7IRX73RQZBL3RQUH6KS3O4NYFY3QCALDLZD77XMZOPWAVTUK +export type AssetID = string; diff --git a/src/model/balance.ts b/src/model/balance.ts index 138d3fbd..e5b486d1 100644 --- a/src/model/balance.ts +++ b/src/model/balance.ts @@ -1,5 +1,4 @@ import { Asset } from "stellar-sdk"; -import { toFloatAmountString } from "../util/stellar"; import { AccountID } from "./account_id"; export interface IBalanceBase { @@ -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"); + } } diff --git a/src/model/balance_values.ts b/src/model/balance_values.ts index 0f275dbe..bd8e681d 100644 --- a/src/model/balance_values.ts +++ b/src/model/balance_values.ts @@ -1,5 +1,4 @@ import { Asset } from "stellar-sdk"; -import { toFloatAmountString } from "../util/stellar"; import { IBalanceBase } from "./balance"; export class BalanceValues implements IBalanceBase { @@ -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; } diff --git a/src/model/index.ts b/src/model/index.ts index bd3897be..fbbb533a 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -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"; diff --git a/src/repo/assets.ts b/src/repo/assets.ts index 68d1e597..191a9915 100644 --- a/src/repo/assets.ts +++ b/src/repo/assets.ts @@ -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; @@ -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); + } } diff --git a/src/schema/assets.ts b/src/schema/assets.ts index 39042f15..c7b92748 100644 --- a/src/schema/assets.ts +++ b/src/schema/assets.ts @@ -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" @@ -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" @@ -50,6 +54,8 @@ export const typeDefs = gql` } type Query { + "Get single asset" + asset(id: AssetID): Asset "Get list of assets" assets( code: AssetCode diff --git a/src/schema/resolvers/asset.ts b/src/schema/resolvers/asset.ts index 7b24a89f..8e2c6486 100644 --- a/src/schema/resolvers/asset.ts +++ b/src/schema/resolvers/asset.ts @@ -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( diff --git a/src/schema/resolvers/balance.ts b/src/schema/resolvers/balance.ts index 3bac396f..867c6dc0 100644 --- a/src/schema/resolvers/balance.ts +++ b/src/schema/resolvers/balance.ts @@ -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"; @@ -24,7 +25,9 @@ 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, @@ -32,7 +35,9 @@ export default { }, 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) } }; diff --git a/src/schema/resolvers/util.ts b/src/schema/resolvers/util.ts index c96fdf6c..e192e737 100644 --- a/src/schema/resolvers/util.ts +++ b/src/schema/resolvers/util.ts @@ -22,10 +22,10 @@ export function idOnlyRequested(info: any): boolean { return false; } -export function makeConnection(records: T[], nodeBuilder: (r: T) => R) { +export function makeConnection(records: T[], nodeBuilder?: (r: T) => R) { const edges = records.map(record => { return { - node: nodeBuilder(record), + node: nodeBuilder ? nodeBuilder(record) : record, cursor: record.paging_token }; }); diff --git a/src/schema/type_defs.ts b/src/schema/type_defs.ts index fd88c5bf..5f71533c 100644 --- a/src/schema/type_defs.ts +++ b/src/schema/type_defs.ts @@ -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 diff --git a/src/util/paging.ts b/src/util/paging.ts index e648db50..18d15a0f 100644 --- a/src/util/paging.ts +++ b/src/util/paging.ts @@ -1,3 +1,5 @@ +import { PagingParams } from "../datasource/horizon/base"; + export enum SortOrder { DESC = "desc", ASC = "asc" @@ -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; +} diff --git a/tests/unit/model/balance.test.ts b/tests/unit/model/balance.test.ts index 7a7863c9..0da6a4fe 100644 --- a/tests/unit/model/balance.test.ts +++ b/tests/unit/model/balance.test.ts @@ -25,8 +25,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);