diff --git a/src/model/account.ts b/src/model/account.ts index aaec10c3..449d6b71 100644 --- a/src/model/account.ts +++ b/src/model/account.ts @@ -1,5 +1,5 @@ import { BigNumber } from "bignumber.js"; -import { AccountFlags, AccountID, AccountThresholds, Signer } from "./"; +import { AccountID, AccountThresholds, Signer } from "./"; export interface IAccountBase { id: string; @@ -9,7 +9,6 @@ export interface IAccountBase { inflationDestination: AccountID; homeDomain: string; thresholds: AccountThresholds; - flags: AccountFlags; signers?: Signer[]; } @@ -27,7 +26,6 @@ export class Account implements IAccount { public inflationDestination: AccountID; public homeDomain: string; public thresholds: AccountThresholds; - public flags: AccountFlags; public lastModified: number; public signers?: Signer[]; public readonly sellingLiabilities: BigNumber; @@ -42,7 +40,6 @@ export class Account implements IAccount { this.homeDomain = data.homeDomain; this.lastModified = data.lastModified; this.thresholds = data.thresholds; - this.flags = data.flags; this.signers = data.signers; this.sellingLiabilities = data.sellingLiabilities; this.buyingLiabilities = data.buyingLiabilities; diff --git a/src/model/account_values.ts b/src/model/account_values.ts index e3f4c547..feb310d0 100644 --- a/src/model/account_values.ts +++ b/src/model/account_values.ts @@ -1,5 +1,4 @@ import { IAccountBase } from "./account"; -import { AccountFlags } from "./account_flags"; import { AccountThresholds } from "./account_thresholds"; import { Signer } from "./signer"; @@ -15,7 +14,6 @@ export class AccountValues implements IAccountValues { public inflationDestination: string; public homeDomain: string; public thresholds: AccountThresholds; - public flags: AccountFlags; public signers: Signer[]; constructor(data: IAccountValues) { @@ -26,7 +24,6 @@ export class AccountValues implements IAccountValues { this.inflationDestination = data.inflationDestination; this.homeDomain = data.homeDomain; this.thresholds = data.thresholds; - this.flags = data.flags; this.signers = Signer.sortArray(data.signers); } @@ -46,10 +43,6 @@ export class AccountValues implements IAccountValues { } } - if (!this.flags.equals(other.flags)) { - changedAttrs.push("flags"); - } - if (!this.thresholds.equals(other.thresholds)) { changedAttrs.push("thresholds"); } diff --git a/src/model/asset.ts b/src/model/asset.ts new file mode 100644 index 00000000..994e4482 --- /dev/null +++ b/src/model/asset.ts @@ -0,0 +1,64 @@ +import { AccountID, AssetCode, IAssetInput } from "./"; +import { AccountFlagsFactory } from "./factories"; + +interface IAssetData { + code: AssetCode; + issuer: AccountID; + totalSupply: string; + circulatingSupply: string; + holdersCount: string; + unauthorizedHoldersCount: string; + lastModifiedIn: number; + flags: number; +} + +export class Asset { + public static readonly NATIVE_ID = "native"; + public readonly code: AssetCode; + public readonly issuer: AccountID | null; + public readonly totalSupply: string; + public readonly circulatingSupply: string; + public readonly holdersCount: string; + public readonly unauthorizedHoldersCount: string; + public readonly lastModifiedIn: number; + public readonly authRequired: boolean; + public readonly authRevocable: boolean; + public readonly authImmutable: boolean; + + constructor(data: IAssetData) { + this.code = data.code; + this.issuer = data.issuer; + this.totalSupply = data.totalSupply; + this.circulatingSupply = data.circulatingSupply; + this.holdersCount = data.holdersCount; + this.unauthorizedHoldersCount = data.unauthorizedHoldersCount; + this.lastModifiedIn = data.lastModifiedIn; + + const parsedFlags = AccountFlagsFactory.fromValue(data.flags); + this.authRequired = parsedFlags.authRequired; + this.authRevocable = parsedFlags.authRevocable; + this.authImmutable = parsedFlags.authImmutable; + } + + public get native(): boolean { + return this.code === "XLM" && this.issuer === null; + } + + public get id() { + return this.native ? "native" : `${this.code}-${this.issuer}`; + } + + public get paging_token() { + return this.id; + } + + public toInput(): IAssetInput { + const res: IAssetInput = { code: this.code }; + + if (this.issuer) { + res.issuer = this.issuer; + } + + return res; + } +} diff --git a/src/model/balance.ts b/src/model/balance.ts index 2b752499..dad9d242 100644 --- a/src/model/balance.ts +++ b/src/model/balance.ts @@ -1,10 +1,9 @@ import { BigNumber } from "bignumber.js"; -import { Asset } from "stellar-sdk"; import { AccountID, AssetID } from "./"; export interface IBalanceBase { account: AccountID; - asset: Asset; + asset: AssetID; limit: BigNumber; balance: BigNumber; authorized: boolean; @@ -24,7 +23,7 @@ export class Balance implements IBalance { } public account: AccountID; - public asset: Asset; + public asset: AssetID; public readonly limit: BigNumber; public readonly balance: BigNumber; public authorized: boolean; diff --git a/src/model/balance_values.ts b/src/model/balance_values.ts index deca9d34..7ba0ee13 100644 --- a/src/model/balance_values.ts +++ b/src/model/balance_values.ts @@ -1,10 +1,9 @@ import { BigNumber } from "bignumber.js"; -import { Asset } from "stellar-sdk"; -import { IBalanceBase } from "./balance"; +import { AssetID, IBalanceBase } from "./"; export class BalanceValues implements IBalanceBase { public account: string; - public asset: Asset; + public asset: AssetID; public limit: BigNumber; public balance: BigNumber; public authorized: boolean; diff --git a/src/model/factories/account_factory.ts b/src/model/factories/account_factory.ts index 928fe9a0..5b4b2c05 100644 --- a/src/model/factories/account_factory.ts +++ b/src/model/factories/account_factory.ts @@ -2,9 +2,7 @@ import { BigNumber } from "bignumber.js"; import { VarArray } from "js-xdr"; import { xdr } from "stellar-base"; import { Account, IAccount } from "../account"; -import { SignerFactory } from "./"; -import { AccountFlagsFactory } from "./account_flags_factory"; -import { AccountThresholdsFactory } from "./account_thresholds_factory"; +import { AccountThresholdsFactory, SignerFactory } from "./"; export interface IAccountTableRow { accountid: string; @@ -32,7 +30,6 @@ export class AccountFactory { homeDomain: row.homedomain, lastModified: row.lastmodified, thresholds: AccountThresholdsFactory.fromValue(row.thresholds), - flags: AccountFlagsFactory.fromValue(row.flags), sellingLiabilities: new BigNumber(row.sellingliabilities), buyingLiabilities: new BigNumber(row.buyingliabilities) }; diff --git a/src/model/factories/account_values_factory.ts b/src/model/factories/account_values_factory.ts index 04bfc306..1bbb7232 100644 --- a/src/model/factories/account_values_factory.ts +++ b/src/model/factories/account_values_factory.ts @@ -1,6 +1,4 @@ -import { AccountFlagsFactory } from "./account_flags_factory"; -import { AccountThresholdsFactory } from "./account_thresholds_factory"; -import { SignerFactory } from "./signer_factory"; +import { AccountThresholdsFactory, SignerFactory } from "./"; import { AccountValues, IAccountValues } from "../"; @@ -22,7 +20,6 @@ export class AccountValuesFactory { inflationDestination: xdr.inflationDest() || null, homeDomain: xdr.homeDomain(), thresholds, - flags: AccountFlagsFactory.fromValue(xdr.flags()), signers }; diff --git a/src/model/factories/asset_factory.ts b/src/model/factories/asset_factory.ts index 6397d995..f8fa33cd 100644 --- a/src/model/factories/asset_factory.ts +++ b/src/model/factories/asset_factory.ts @@ -1,27 +1,53 @@ -import { Asset, xdr as XDR } from "stellar-base"; +import { xdr as XDR } from "stellar-base"; +import { Asset as StellarAsset } from "stellar-sdk"; +import { AccountID, Asset, AssetCode, AssetID, IAssetInput } from "../"; import { HorizonAssetType } from "../../datasource/types"; -import { IAssetInput } from "../asset_input"; + +export interface IAssetTableRow { + assetid: AssetID; + code: AssetCode; + issuer: AccountID; + total_supply: string; + circulating_supply: string; + holders_count: string; + unauthorized_holders_count: string; + flags: number; + last_activity: number; +} export class AssetFactory { - public static fromDb(type: number, code: string, issuer: string) { - return type === XDR.AssetType.assetTypeNative().value ? Asset.native() : new Asset(code, issuer); + public static fromDb(row: IAssetTableRow): Asset { + return new Asset({ + code: row.code, + issuer: row.issuer, + totalSupply: row.total_supply, + circulatingSupply: row.circulating_supply, + holdersCount: row.holders_count, + unauthorizedHoldersCount: row.unauthorized_holders_count, + lastModifiedIn: row.last_activity, + flags: row.flags + }); + } + + public static fromTrustline(type: number, code: string, issuer: string): StellarAsset { + return type === XDR.AssetType.assetTypeNative().value ? StellarAsset.native() : new StellarAsset(code, issuer); } public static fromHorizon(type: HorizonAssetType, code?: string, issuer?: string) { - return type === "native" ? Asset.native() : new Asset(code, issuer); + return type === "native" ? StellarAsset.native() : new StellarAsset(code!, issuer!); } public static fromInput(arg: IAssetInput) { if (arg.issuer && arg.code) { - return new Asset(arg.code, arg.issuer); + return new StellarAsset(arg.code, arg.issuer); } - return Asset.native(); + return StellarAsset.native(); } public static fromId(id: string) { if (id === "native") { - return Asset.native(); + return StellarAsset.native(); } const [code, issuer] = id.split("-"); @@ -30,10 +56,10 @@ export class AssetFactory { throw new Error(`Invalid asset id "${id}"`); } - return new Asset(code, issuer); + return new StellarAsset(code, issuer); } public static fromXDR(xdr: any, encoding = "base64") { - return Asset.fromOperation(XDR.Asset.fromXDR(xdr, encoding)); + return StellarAsset.fromOperation(XDR.Asset.fromXDR(xdr, encoding)); } } diff --git a/src/model/factories/balance_factory.ts b/src/model/factories/balance_factory.ts index b5c7b162..b5633251 100644 --- a/src/model/factories/balance_factory.ts +++ b/src/model/factories/balance_factory.ts @@ -1,9 +1,8 @@ import BigNumber from "bignumber.js"; -import { Asset, xdr as XDR } from "stellar-base"; +import { xdr as XDR } from "stellar-base"; import { Account, Balance, IBalance } from "../"; import { MAX_INT64 } from "../../util"; import { getReservedBalance } from "../../util/base_reserve"; -import { AssetFactory } from "./"; export interface ITrustLineTableRow { accountid: string; @@ -28,7 +27,7 @@ export class BalanceFactory { balance, limit, lastModified: row.lastmodified, - asset: AssetFactory.fromDb(row.assettype, row.assetcode, row.issuer), + asset: `${row.assetcode}-${row.issuer}`, authorized: (row.flags & XDR.TrustLineFlags.authorizedFlag().value) > 0, spendableBalance: balance.minus(row.sellingliabilities || 0), receivableBalance: limit.minus(row.buyingliabilities || 0).minus(balance) @@ -44,7 +43,7 @@ export class BalanceFactory { return new Balance({ account: account.id, - asset: Asset.native(), + asset: "native", balance, limit, authorized: true, diff --git a/src/model/factories/balance_values_factory.ts b/src/model/factories/balance_values_factory.ts index 3dbdd2cd..30794dc0 100644 --- a/src/model/factories/balance_values_factory.ts +++ b/src/model/factories/balance_values_factory.ts @@ -8,7 +8,7 @@ import { BalanceValues } from "../balance_values"; export class BalanceValuesFactory { public static fromXDR(xdr: any): BalanceValues { return new BalanceValues({ - asset: Asset.fromOperation(xdr.asset()), + asset: Asset.fromOperation(xdr.asset()).toString(), account: publicKeyFromXDR(xdr), balance: new BigNumber(xdr.balance().toString()), limit: new BigNumber(xdr.limit().toString()), @@ -19,7 +19,7 @@ export class BalanceValuesFactory { public static fakeNativeFromXDR(xdr: any): BalanceValues { return new BalanceValues({ account: publicKeyFromXDR(xdr), - asset: Asset.native(), + asset: "native", limit: new BigNumber(MAX_INT64), authorized: true, balance: new BigNumber(xdr.balance().toString()) diff --git a/src/model/index.ts b/src/model/index.ts index fbbb533a..515416cb 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -4,6 +4,8 @@ export * from "./account_thresholds"; export * from "./account_flags"; export * from "./account_values"; export * from "./account_subscription_payload"; +export * from "./asset"; +export * from "./asset_code"; export * from "./asset_id"; export * from "./asset_input"; export * from "./data_entry"; diff --git a/src/repo/assets.ts b/src/repo/assets.ts index a8d9d781..d20cec1a 100644 --- a/src/repo/assets.ts +++ b/src/repo/assets.ts @@ -1,10 +1,11 @@ import { IDatabase } from "pg-promise"; import squel from "squel"; -import { Asset } from "stellar-sdk"; -import { Balance } from "../model"; -import { BalanceFactory } from "../model/factories"; +import { Asset, AssetID, Balance, IAssetInput } from "../model"; +import { AssetFactory, BalanceFactory, IAssetTableRow } from "../model/factories"; import { PagingParams, parseCursorPagination, properlyOrdered, SortOrder } from "../util/paging"; +const TABLE_NAME = "assets"; + export default class AssetsRepo { private db: IDatabase; @@ -12,46 +13,78 @@ export default class AssetsRepo { this.db = db; } - public async findAll(code?: string, issuer?: string, limit?: number, offset?: number) { + public async findByID(assetId: AssetID) { const queryBuilder = squel .select() - .field("assetcode") - .field("issuer") - .from("trustlines") - .group("assetcode") - .group("issuer") - .order("assetcode"); + .from(TABLE_NAME) + .where("assetid = ?", assetId); + + const res = await this.db.oneOrNone(queryBuilder.toString()); + + return res ? AssetFactory.fromDb(res) : null; + } - if (code) { - queryBuilder.having("assetcode = ?", code); + public async findAllByIDs(assetIds: AssetID[]) { + if (assetIds.length === 0) { + return []; } - if (issuer) { - queryBuilder.having("issuer = ?", issuer); + const queryBuilder = squel + .select() + .from(TABLE_NAME) + .where("assetid IN ?", assetIds); + + const res = await this.db.manyOrNone(queryBuilder.toString()); + const assets = res.map(r => AssetFactory.fromDb(r)); + + return assetIds.map(id => assets.find(a => a.id === id) || null); + } + + public async findAll(criteria: IAssetInput, paging: PagingParams) { + const { limit, cursor, order } = parseCursorPagination(paging); + // We skip lumens here for the sake of consistent pagination + // they have `native` as an id and pagination token so it will + // break intended alphanumeric ordering + // Anyway, it seems a rare case, when someone would need lumens in + // the aggregate list + const queryBuilder = squel + .select() + .from(TABLE_NAME) + .where("assetid != ?", Asset.NATIVE_ID) + .order("assetid", order === SortOrder.ASC) + .limit(limit); + + if (criteria.code) { + queryBuilder.where("code = ?", criteria.code); } - if (limit) { - queryBuilder.limit(limit); + if (criteria.issuer) { + queryBuilder.where("issuer = ?", criteria.issuer); } - if (offset) { - queryBuilder.offset(offset); + if (cursor) { + if (paging.after) { + queryBuilder.where("assetid > ?", paging.after); + } else if (paging.before) { + queryBuilder.where("assetid < ?", paging.before); + } } const res = await this.db.manyOrNone(queryBuilder.toString()); + const assets = res.map(r => AssetFactory.fromDb(r)); - return res.map(a => new Asset(a.assetcode, a.issuer)); + return properlyOrdered(assets, paging); } - public async findHolders(asset: Asset, paging: PagingParams) { + public async findHolders(asset: IAssetInput, paging: PagingParams) { const { limit, cursor, order } = parseCursorPagination(paging); const ascending = order === SortOrder.ASC; const queryBuilder = squel .select() .from("trustlines") - .where("assetcode = ?", asset.getCode()) - .where("issuer = ?", asset.getIssuer()) + .where("assetcode = ?", asset.code) + .where("issuer = ?", asset.issuer) .order("balance", ascending) .order("accountid", ascending) .limit(limit); diff --git a/src/schema/accounts.ts b/src/schema/accounts.ts index 38c398a1..7bd9a227 100644 --- a/src/schema/accounts.ts +++ b/src/schema/accounts.ts @@ -4,16 +4,6 @@ export const typeDefs = gql` "Represents the [public key](https://www.stellar.org/developers/guides/concepts/accounts.html#account-id) of the particular account" scalar AccountID - "Represents three flags, used by issuers of assets" - type AccountFlags { - "Requires the issuing account to give other accounts permission before they can hold the issuing account’s credit" - authRequired: Boolean! - "Allows the issuing account to revoke its credit held by other accounts" - authRevocable: Boolean! - "If this is set then none of the authorization flags can be set and the account can never be deleted" - authImmutable: Boolean! - } - "Represents [thresholds](https://www.stellar.org/developers/guides/concepts/accounts.html#thresholds) for different access levels" type AccountThresholds { "The weight of the master key" @@ -42,14 +32,14 @@ export const typeDefs = gql` homeDomain: String "Thresholds for different access levels this account set" thresholds: AccountThresholds! - "Flags used, by issuers of assets" - flags: AccountFlags! "[Signers](https://www.stellar.org/developers/guides/concepts/multi-sig.html) of the account" signers: [Signer] "Ledger, in which account was modified last time" ledger: Ledger! "[Data entries](https://www.stellar.org/developers/guides/concepts/list-of-operations.html#manage-data), attached to the account" data: [DataEntry] + "All assets, issued by this account" + assets(first: Int, after: String, last: Int, before: String): AssetConnection "[Balances](https://www.stellar.org/developers/guides/concepts/assets.html#trustlines) of this account" balances: [Balance] "A list of [operations](https://www.stellar.org/developers/guides/concepts/operations.html) on the Stellar network that the account performed" @@ -84,8 +74,6 @@ export const typeDefs = gql` homeDomain: String "Thresholds for different access levels this account set" thresholds: AccountThresholds! - "Flags used, by issuers of assets" - flags: AccountFlags! "[Signers](https://www.stellar.org/developers/guides/concepts/multi-sig.html) of the account" signers: [Signer] } diff --git a/src/schema/assets.ts b/src/schema/assets.ts index 4716ad24..86df4f67 100644 --- a/src/schema/assets.ts +++ b/src/schema/assets.ts @@ -10,42 +10,40 @@ export const typeDefs = gql` type Asset { "Whether this asset is [lumens](https://www.stellar.org/developers/guides/concepts/assets.html)" native: Boolean! - "Asset issuer's account" + "Asset issuer's account. It's \`null\` for native lumens" 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" - type AssetWithInfo { - "Whether this asset is [lumens](https://www.stellar.org/developers/guides/concepts/assets.html)" - native: Boolean! - "Asset issuer's account" - issuer: Account - "Asset's code" - code: AssetCode! - "The number of units of credit issued" - amount: Float - "The number of accounts that: 1) trust this asset and 2) where if the asset has the auth_required flag then the account is authorized to hold the asset" - numAccounts: Int - "Asset's issuer account flags" - flags: AccountFlags + "Sum of all asset holders balances" + totalSupply: String! + "Sum of only authorized holders balances" + circulatingSupply: String! + "Total number of asset holders" + holdersCount: Int! + "Total number of unathorized holders" + unauthorizedHoldersCount: Int! + "Ledger this asset was last time modified in" + lastModifiedIn: Ledger! + "Requires the issuing account to give other accounts permission before they can hold the issuing account’s credit" + authRequired: Boolean! + "Allows the issuing account to revoke its credit held by other accounts" + authRevocable: Boolean! + "If this is set then none of the authorization flags can be set and the account can never be deleted" + authImmutable: Boolean! "All accounts that trust this asset, ordered by balance" balances(first: Int, last: Int, after: String, before: String): BalanceConnection } "A list of assets" - type AssetWithInfoConnection { + type AssetConnection { pageInfo: PageInfo! - nodes: [AssetWithInfo] - edges: [AssetWithInfoEdge] + nodes: [Asset] + edges: [AssetEdge] } - type AssetWithInfoEdge { + type AssetEdge { cursor: String! - node: AssetWithInfo + node: Asset } input AssetInput { @@ -56,14 +54,7 @@ export const typeDefs = gql` type Query { "Get single asset" asset(id: AssetID): Asset - "Get list of assets" - assets( - code: AssetCode - issuer: AccountID - first: Int - after: String - last: Int - before: String - ): AssetWithInfoConnection + "Get list of assets. Note: native XLM asset isn't included here" + assets(code: AssetCode, issuer: AccountID, first: Int, after: String, last: Int, before: String): AssetConnection } `; diff --git a/src/schema/resolvers/account.ts b/src/schema/resolvers/account.ts index 85f7de12..10399d63 100644 --- a/src/schema/resolvers/account.ts +++ b/src/schema/resolvers/account.ts @@ -70,6 +70,10 @@ export default { Account: { homeDomain: (root: Account) => Buffer.from(root.homeDomain, "base64").toString(), reservedBalance: (root: Account) => toFloatAmountString(getReservedBalance(root.numSubentries)), + assets: async (root: Account, args: any) => { + const assets = await db.assets.findAll({ issuer: root.id }, args); + return makeConnection(assets); + }, data: dataEntriesResolver, balances: balancesResolver, ledger: resolvers.ledger, @@ -102,9 +106,8 @@ export default { inflationDestination: resolvers.account }, Query: { - account: async (root: any, args: any) => { - const acc = await db.accounts.findByID(args.id); - return acc; + account(root: any, args: any) { + return db.accounts.findByID(args.id); }, accounts: async (root: any, args: any) => { const { ids, homeDomain, ...paging } = args; diff --git a/src/schema/resolvers/asset.ts b/src/schema/resolvers/asset.ts index 17fab4a2..888bc2ad 100644 --- a/src/schema/resolvers/asset.ts +++ b/src/schema/resolvers/asset.ts @@ -1,46 +1,33 @@ -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 { Asset, AssetID } from "../../model"; +import { toFloatAmountString } from "../../util/stellar"; import * as resolvers from "./shared"; import { makeConnection } from "./util"; -const holdersResolver = async (root: any, args: any, ctx: IApolloContext, info: any) => { - const asset = root.native ? Asset.native() : new Asset(root.code, root.issuer); - const balances = await db.assets.findHolders(asset, args); +const holdersResolver = async (root: Asset, args: any, ctx: IApolloContext, info: any) => { + const balances = await db.assets.findHolders(root.toInput(), args); + return makeConnection(balances); }; export default { - Asset: { issuer: resolvers.account, balances: holdersResolver }, - AssetWithInfo: { issuer: resolvers.account, balances: holdersResolver }, + Asset: { + issuer: resolvers.account, + lastModifiedIn: resolvers.ledger, + totalSupply: (asset: Asset) => toFloatAmountString(asset.totalSupply), + circulatingSupply: (asset: Asset) => toFloatAmountString(asset.circulatingSupply), + balances: holdersResolver + }, Query: { - asset: async (root: any, args: { id: AssetID }, ctx: IApolloContext, info: any) => { - return AssetFactory.fromId(args.id); + asset: async (root: any, { id }: { id: AssetID }, ctx: IApolloContext, info: any) => { + return db.assets.findByID(id); }, assets: async (root: any, args: any, ctx: IApolloContext, info: any) => { const { code, issuer } = args; - const records: IHorizonAssetData[] = await ctx.dataSources.assets.all( - code || issuer ? { code, issuer } : {}, - args - ); + const records = await db.assets.findAll(code || issuer ? { code, issuer } : {}, args); - return makeConnection(records, (r: IHorizonAssetData) => { - return { - code: r.asset_code, - issuer: r.asset_issuer, - native: r.asset_type === "native", - amount: r.amount, - numAccounts: r.num_accounts, - flags: { - authRequired: r.flags.auth_required, - authRevocable: r.flags.auth_revocable, - authImmutable: r.flags.auth_immutable - } - }; - }); + return makeConnection(records); } } }; diff --git a/src/schema/resolvers/shared/asset.ts b/src/schema/resolvers/shared/asset.ts index 61be772a..ad596281 100644 --- a/src/schema/resolvers/shared/asset.ts +++ b/src/schema/resolvers/shared/asset.ts @@ -1,17 +1,28 @@ -import { Asset } from "stellar-sdk"; +import { db } from "../../../database"; import { IApolloContext } from "../../../graphql_server"; +import { Asset, AssetID } from "../../../model"; +import { createBatchResolver, onlyFieldsRequested } from "../util"; -export function asset(obj: any, args: any, ctx: IApolloContext, info: any) { +export const asset = createBatchResolver((source: any, args: any, ctx: IApolloContext, info: any) => { const field = info.fieldName; - const value = obj[field] as Asset; - const res = (a: Asset): any => { - return { code: a.getCode(), issuer: a.getIssuer(), native: a.isNative() }; - }; + if (onlyFieldsRequested(info, ["code", "issuer", "native"])) { + return source.map((obj: any) => { + // this trick will return asset id either `obj[field]` + // is instance of SDK Asset class, either it's already a + // string asset id + const assetId = obj[field].toString(); - if (Array.isArray(value)) { - return value.map(a => res(a)); + if (assetId === "native") { + return { code: "XLM", issuer: null, native: true }; + } + + const [code, issuer] = assetId.split("-"); + return { code, issuer, native: false }; + }); } - return res(value); -} + const ids: AssetID[] = source.map((s: any) => s[field].toString()); + + return db.assets.findAllByIDs(ids); +}); diff --git a/src/schema/resolvers/shared/ledger.ts b/src/schema/resolvers/shared/ledger.ts index 539c528d..7b6df4ce 100644 --- a/src/schema/resolvers/shared/ledger.ts +++ b/src/schema/resolvers/shared/ledger.ts @@ -1,6 +1,6 @@ import { Ledger } from "../../../model"; export function ledger(obj: any) { - const seq = obj.lastModified || obj.ledgerSeq; + const seq = obj.lastModified || obj.lastModifiedIn || obj.ledgerSeq; return new Ledger(seq); } diff --git a/src/schema/resolvers/util.ts b/src/schema/resolvers/util.ts index fca75152..acc372d6 100644 --- a/src/schema/resolvers/util.ts +++ b/src/schema/resolvers/util.ts @@ -22,6 +22,12 @@ export function idOnlyRequested(info: any): boolean { return false; } +export function onlyFieldsRequested(info: any, fields: string[]): boolean { + const requestedFields = [...new Set(fieldsList(info))]; // dedupe + + return JSON.stringify(requestedFields.sort()) === JSON.stringify(fields.sort()); +} + export function makeConnection(records: T[], nodeBuilder?: (r: T) => R) { const edges = records.map(record => { return { diff --git a/tests/integration/__snapshots__/integration.test.ts.snap b/tests/integration/__snapshots__/integration.test.ts.snap index f68ef5a3..1171e110 100644 --- a/tests/integration/__snapshots__/integration.test.ts.snap +++ b/tests/integration/__snapshots__/integration.test.ts.snap @@ -5,23 +5,9 @@ Object { "assets": Object { "nodes": Array [ Object { - "code": "ZZZ", + "code": "KHL", "issuer": Object { - "id": "GCLCYDPGX7YBYMV627KV2GAXFEYYKJYZBCZEFVKELIHJ62SLKFAREUWH", - }, - "native": false, - }, - Object { - "code": "ZZZ", - "issuer": Object { - "id": "GBYMZE2O2CCLYVZKDBSE4SBWYSMFGYSAX3QN5KY4EV32ADEKVY5WIXWC", - }, - "native": false, - }, - Object { - "code": "ZZZ", - "issuer": Object { - "id": "GBKSCKCMFEIZSELRVTYO5XWGSEYURZY7AEV35MYW65DBS6FT6OHKP5IZ", + "id": "GBTVGCI5OYBGDMCNKYFVSETXK6TTOQ4DRJBAC5K2PUUNSVB24PPSQ26Q", }, "native": false, }, @@ -76,11 +62,6 @@ Object { "value": "test_value", }, ], - "flags": Object { - "authImmutable": false, - "authRequired": false, - "authRevocable": false, - }, "id": "GCVIRZIN4CGYY56CSL7RYAFHKQTGE34GIASZ2D4IGZGV3FDL622LPQZT", "inflationDestination": null, "ledger": Object { diff --git a/tests/integration/integration_queries/assets.gql b/tests/integration/integration_queries/assets.gql index a1274cd2..b0b45d68 100644 --- a/tests/integration/integration_queries/assets.gql +++ b/tests/integration/integration_queries/assets.gql @@ -1,6 +1,7 @@ query { assets( - code: "KHL" + code: "KHL", + first: 10 ) { nodes { code diff --git a/tests/integration/integration_queries/single_account_query.gql b/tests/integration/integration_queries/single_account_query.gql index 999b4952..69f99009 100644 --- a/tests/integration/integration_queries/single_account_query.gql +++ b/tests/integration/integration_queries/single_account_query.gql @@ -10,11 +10,6 @@ issuer { id } } } - flags { - authRequired - authRevocable - authImmutable - } thresholds { masterWeight low diff --git a/tests/integration/test_db.sql b/tests/integration/test_db.sql index 1e40b75c..bf324b29 100644 --- a/tests/integration/test_db.sql +++ b/tests/integration/test_db.sql @@ -42,6 +42,7 @@ ALTER TABLE IF EXISTS ONLY public.ledgerheaders DROP CONSTRAINT IF EXISTS ledger ALTER TABLE IF EXISTS ONLY public.ban DROP CONSTRAINT IF EXISTS ban_pkey; ALTER TABLE IF EXISTS ONLY public.accounts DROP CONSTRAINT IF EXISTS accounts_pkey; ALTER TABLE IF EXISTS ONLY public.accountdata DROP CONSTRAINT IF EXISTS accountdata_pkey; +DROP VIEW IF EXISTS public.assets; DROP TABLE IF EXISTS public.upgradehistory; DROP TABLE IF EXISTS public.txhistory; DROP TABLE IF EXISTS public.txfeehistory; @@ -551,6 +552,28 @@ INSERT INTO public.txhistory VALUES ('b5f4d71a8ef136431f9495c3747c70a97a9dc4f333 INSERT INTO public.upgradehistory VALUES (2, 1, 'AAAAAQAAAAo=', 'AAAAAA=='); INSERT INTO public.upgradehistory VALUES (2, 2, 'AAAAAwAAJxA=', 'AAAAAA=='); +CREATE VIEW public.assets AS +( SELECT (((t.assetcode)::text || '-'::text) || (t.issuer)::text) AS assetid, + t.assetcode AS code, + t.issuer, + sum(t.balance) AS total_supply, + sum(t.balance) FILTER (WHERE (t.flags = 1)) AS circulating_supply, + count(t.accountid) AS holders_count, + count(t.accountid) FILTER (WHERE (t.flags = 0)) AS unauthorized_holders_count, + max(t.lastmodified) AS last_activity + FROM public.trustlines t + GROUP BY t.issuer, t.assetcode + ORDER BY (count(t.accountid)) DESC) +UNION +SELECT 'native'::text AS assetid, +'XLM'::character varying AS code, +NULL::character varying AS issuer, +sum(accounts.balance) AS total_supply, +sum(accounts.balance) AS circulating_supply, +count(*) AS holders_count, +0 AS unauthorized_holders_count, +max(accounts.lastmodified) AS last_activity +FROM public.accounts; -- -- Name: accountdata accountdata_pkey; Type: CONSTRAINT; Schema: public; Owner: - diff --git a/tests/unit/model/account_values.test.ts b/tests/unit/model/account_values.test.ts index a73c9c7a..7a48cfc6 100644 --- a/tests/unit/model/account_values.test.ts +++ b/tests/unit/model/account_values.test.ts @@ -1,6 +1,6 @@ import stellar from "stellar-base"; import { AccountValues } from "../../../src/model"; -import { AccountFlagsFactory, AccountThresholdsFactory } from "../../../src/model/factories"; +import { AccountThresholdsFactory } from "../../../src/model/factories"; import AccountValuesFactory from "../../factories/account_values"; import SignerFactory from "../../factories/signer"; @@ -40,7 +40,6 @@ describe("diffAttrs(other)", () => { inflationDestination: "", homeDomain: "", thresholds: AccountThresholdsFactory.fromValue("AQAAAA=="), - flags: AccountFlagsFactory.fromValue(0), lastModified: 6, signers: [] }; @@ -52,7 +51,6 @@ describe("diffAttrs(other)", () => { inflationDestination: "some_inflation_dest", homeDomain: "example.com", thresholds: AccountThresholdsFactory.fromValue("AQEBAQ=="), - flags: AccountFlagsFactory.fromValue(2), lastModified: 5, signers: [SignerFactory.build()] }; @@ -65,7 +63,6 @@ describe("diffAttrs(other)", () => { "numSubentries", "inflationDestination", "homeDomain", - "flags", "thresholds", "signers" ]); diff --git a/tests/unit/model/balance.test.ts b/tests/unit/model/balance.test.ts index e9d09a16..9b85e4af 100644 --- a/tests/unit/model/balance.test.ts +++ b/tests/unit/model/balance.test.ts @@ -1,4 +1,3 @@ -import { Asset } from "stellar-sdk"; import { Balance } from "../../../src/model"; import { BalanceFactory } from "../../../src/model/factories"; import { MAX_INT64 } from "../../../src/util"; @@ -34,11 +33,7 @@ describe("constructor", () => { it("sets limit", () => expect(subject.limit.toString()).toEqual("9223372036854775807")); it("sets balance", () => expect(subject.balance.toString()).toEqual("9600000000")); it("sets authorized", () => expect(subject.authorized).toBe(true)); - it("sets asset", () => { - expect(subject.asset).toBeInstanceOf(Asset); - expect(subject.asset.getCode()).toEqual(data.assetcode); - expect(subject.asset.getIssuer()).toEqual(data.issuer); - }); + it("sets asset", () => expect(subject.asset).toEqual(`${data.assetcode}-${data.issuer}`)); it("sets spendable balance", () => expect(subject.spendableBalance.toString()).toEqual("9599999700")); it("sets receivable balance", () => expect(subject.receivableBalance.toString()).toEqual("9223372027254775707")); }); @@ -59,6 +54,6 @@ describe("static buildFakeNative(account)", () => { expect(fake.spendableBalance.toString()).toEqual("19706072136"); expect(fake.receivableBalance.toString()).toEqual("9223372017121827946"); - expect(fake.asset.isNative()).toBe(true); + expect(fake.asset).toEqual("native"); }); });