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

Implementing path finding without Horizon #170

Merged
merged 17 commits into from
Jun 18, 2019
Merged
24 changes: 2 additions & 22 deletions src/datasource/horizon/payments.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { AccountID, IAssetInput } from "../../model";
import { AssetFactory } from "../../model/factories";
import { AccountID } from "../../model";
import { PagingParams, parseCursorPagination, properlyOrdered } from "../../util/paging";
import { IHorizonOperationData, IHorizonPaymentPathData } from "../types";
import { IHorizonOperationData } from "../types";
import { BaseHorizonDataSource } from "./base";

export class HorizonPaymentsDataSource extends BaseHorizonDataSource {
Expand Down Expand Up @@ -37,23 +36,4 @@ export class HorizonPaymentsDataSource extends BaseHorizonDataSource {

return properlyOrdered(records, pagingParams);
}

public async findPaths(
sourceAccountID: AccountID,
destinationAccountID: AccountID,
destinationAmount: string,
destinationAssetInput: IAssetInput
): Promise<IHorizonPaymentPathData[]> {
const destinationAsset = AssetFactory.fromInput(destinationAssetInput);

return this.request("paths", {
source_account: sourceAccountID,
destination_account: destinationAccountID,
destination_asset_type: destinationAsset.getAssetType(),
destination_asset_code: destinationAsset.getCode(),
destination_asset_issuer: destinationAsset.getIssuer(),
destination_amount: destinationAmount,
cacheTtl: 120
});
}
}
16 changes: 0 additions & 16 deletions src/datasource/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,22 +164,6 @@ export interface IHorizonAssetData {
flags: { [flag in HorizonAccountFlag]: boolean };
}

export interface IHorizonPaymentPathData {
source_asset_type: HorizonAssetType;
source_asset_code: AssetCode;
source_asset_issuer: AccountID;
source_amount: string;
destination_asset_type: HorizonAssetType;
destination_asset_code: AssetCode;
destination_asset_issuer: AccountID;
destination_amount: string;
path: Array<{
asset_type: HorizonAssetType;
asset_code: AssetCode;
asset_issuer: AccountID;
}>;
}

export interface IHorizonTradeAggregationData {
timestamp: number;
trade_count: number;
Expand Down
3 changes: 2 additions & 1 deletion src/graphql_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {
HorizonTradesDataSource,
HorizonTransactionsDataSource
} from "./datasource/horizon";
import * as orderBook from "./order_book";
import schema from "./schema";
import { listenOffers, orderBook } from "./service/dex";
import logger from "./util/logger";
import { BIND_ADDRESS, PORT } from "./util/secrets";
import { listenBaseReserveChange } from "./util/stellar";
Expand Down Expand Up @@ -59,6 +59,7 @@ export interface IApolloContext {

init().then(() => {
listenBaseReserveChange();
listenOffers();

const server = new ApolloServer({
schema,
Expand Down
19 changes: 15 additions & 4 deletions src/init.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as Integrations from "@sentry/integrations";
import * as Sentry from "@sentry/node";
import { createConnection } from "typeorm";
import { Account, AccountData, Offer } from "./orm/entities";
import { createConnection, getRepository } from "typeorm";
import { Account, AccountData, Offer, TrustLine } from "./orm/entities";
import { buildOffersGraph } from "./service/dex";
import "./util/asset";
import logger from "./util/logger";
import "./util/memo";
Expand All @@ -19,16 +20,26 @@ export default async function init(): Promise<void> {
const network = setStellarNetwork();
logger.info(`Using ${network}`);

await updateBaseReserve();
logger.info("Updating base reserve value...");
const baseReserve = await updateBaseReserve();
logger.info(`Current base reserve value is ${baseReserve}`);

logger.info("Connecting to database...");
await createConnection({
type: "postgres",
host: secrets.DBHOST,
port: secrets.DBPORT,
username: secrets.DBUSER,
password: secrets.DBPASSWORD,
database: secrets.DB,
entities: [Account, AccountData, Offer],
entities: [Account, AccountData, Offer, TrustLine],
synchronize: false,
logging: process.env.DEBUG_SQL !== undefined
});

logger.info("Building offers graph for path finding...");
const offers = await getRepository(Offer).find();
await buildOffersGraph(offers);

logger.info("Astrograph is ready!");
}
12 changes: 9 additions & 3 deletions src/orm/entities/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ import { Column, Entity, OneToMany, PrimaryColumn } from "typeorm";
import { AccountFlags, AccountThresholds, Signer } from "../../model";
import { AccountFlagsFactory, AccountThresholdsFactory, SignerFactory } from "../../model/factories";
import { Base64Transformer, BigNumberTransformer } from "../../util/orm";
import { AccountData } from "./";
import { AccountData, TrustLine } from "./";

@Entity("accounts")
/* tslint:disable */
export class Account {
@PrimaryColumn({ name: "accountid" })
id: string;

@Column("bigint")
balance: string;
@Column({
type: "bigint",
transformer: BigNumberTransformer
})
balance: BigNumber;

@Column({ name: "seqnum", type: "bigint" })
sequenceNumber: string;
Expand Down Expand Up @@ -87,6 +90,9 @@ export class Account {
@OneToMany(type => AccountData, accountData => accountData.account)
data: AccountData[];

@OneToMany(type => TrustLine, trustLine => trustLine.account)
trustLines: TrustLine[];

public get paging_token() {
return this.id;
}
Expand Down
1 change: 1 addition & 0 deletions src/orm/entities/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./account";
export * from "./account_data";
export * from "./offer";
export * from "./trustline";
46 changes: 46 additions & 0 deletions src/orm/entities/trustline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import BigNumber from "bignumber.js";
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm";
import { AccountID, AssetID } from "../../model";
import { AssetFactory } from "../../model/factories";
import { BigNumberTransformer } from "../../util/orm";
import { Account } from "./";

@Entity("trustlines")
/* tslint:disable */
export class TrustLine {
@PrimaryColumn({ name: "accountid" })
@ManyToOne(type => Account, account => account.trustLines)
@JoinColumn({ name: "accountid" })
account: Account;

@PrimaryColumn()
issuer: AccountID;

@PrimaryColumn({ name: "assetcode" })
assetCode: string;

@Column({ name: "assettype" })
assetType: number;

@Column({ name: "tlimit", type: "bigint", transformer: BigNumberTransformer })
limit: BigNumber;

@Column({ type: "bigint", transformer: BigNumberTransformer })
balance: BigNumber;

@Column()
flags: number;

@Column({ name: "lastmodified" })
lastModified: number;

@Column({ type: "bigint", name: "buyingliabilities", transformer: BigNumberTransformer })
buyingLiabilities: BigNumber;

@Column({ type: "bigint", name: "sellingliabilities", transformer: BigNumberTransformer })
sellingLiabilities: BigNumber;

public get asset(): AssetID {
return AssetFactory.fromTrustline(this.assetType, this.assetCode, this.issuer).toString();
}
}
10 changes: 4 additions & 6 deletions src/schema/payment_path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ export const typeDefs = gql`
"The source asset specified in the search that found this path"
sourceAsset: Asset!
"An estimated cost for making a payment of \`destinationAmount\` on this path. Suitable for use in a path payments \`sendMax\` field"
sourceAmount: Float!
sourceAmount: String!
"The destination asset specified in the search that found this path"
destinationAsset: Asset!
"The destination amount specified in the search that found this path"
destinationAmount: Float!
destinationAmount: String!
"An array of assets that represents the intermediary assets this path hops through"
path: [Asset!]
}
Expand All @@ -20,10 +20,8 @@ export const typeDefs = gql`
findPaymentPaths(
"The sender’s account id. Any returned path must use a source that the sender can hold"
sourceAccountID: AccountID!
"The destination account that any returned path should use"
destinationAccountID: AccountID!
destinationAsset: AssetInput!
destinationAmount: Float!
destinationAsset: AssetCode!
destinationAmount: String!
): [PaymentPath!]
}
`;
45 changes: 17 additions & 28 deletions src/schema/resolvers/payment_path.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { IHorizonPaymentPathData } from "../../datasource/types";
import { BigNumber } from "bignumber.js";
import { getRepository } from "typeorm";
import { IApolloContext } from "../../graphql_server";
import { AssetFactory } from "../../model/factories";
import { TrustLine } from "../../orm/entities";
import { findPaymentPaths } from "../../service/dex";
import * as resolvers from "./shared";

export default {
Expand All @@ -11,38 +13,25 @@ export default {
},
Query: {
findPaymentPaths: async (root: any, args: any, ctx: IApolloContext, info: any) => {
const { sourceAccountID, destinationAccountID, destinationAsset, destinationAmount } = args;
const { sourceAccountID, destinationAsset, destinationAmount } = args;

const records = await ctx.dataSources.payments.findPaths(
sourceAccountID,
destinationAccountID,
destinationAmount,
destinationAsset
);
const accountTrustlines = await getRepository(TrustLine).find({ where: { account: sourceAccountID } });

const r = records.map((record: IHorizonPaymentPathData) => {
const path = record.path.map((asset: any) =>
AssetFactory.fromHorizon(asset.asset_type, asset.asset_code, asset.asset_issuer)
);
const nodes = findPaymentPaths(
accountTrustlines.map(t => t.asset).concat("native"),
destinationAsset,
new BigNumber(destinationAmount)
);

return Object.entries(nodes).map(([sourceAsset, data]) => {
return {
sourceAsset: AssetFactory.fromHorizon(
record.source_asset_type,
record.source_asset_code,
record.source_asset_issuer
),
destinationAsset: AssetFactory.fromHorizon(
record.destination_asset_type,
record.destination_asset_code,
record.destination_asset_issuer
),
sourceAmount: record.source_amount,
destinationAmount: record.destination_amount,
path
sourceAsset,
sourceAmount: data.amountNeeded,
destinationAsset,
destinationAmount,
path: data.path
};
});

return r;
}
}
};
29 changes: 19 additions & 10 deletions src/schema/resolvers/shared/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,30 @@ import { createBatchResolver, onlyFieldsRequested } from "../util";

export const asset = createBatchResolver<any, Asset[]>((source: any, args: any, ctx: IApolloContext, info: any) => {
const field = info.fieldName;
// this trick will return asset id either `obj[field]`
// is instance of SDK Asset class, either it's already a
// string asset id
const ids: AssetID[] = source.map((s: any) => s[field].toString());

if (onlyFieldsRequested(info, ["code", "issuer", "native"])) {
return ids.map(id => {
if (id === "native") {
return { code: "XLM", issuer: null, native: true };
if (onlyFieldsRequested(info, "id", "code", "issuer", "native")) {
return source.map((obj: any) => {
if (Array.isArray(obj[field])) {
return obj[field].map(expandAsset);
}

const [code, issuer] = id.split("-");
return { code, issuer, native: false };
// this trick will return asset id either `obj[field]`
// is instance of SDK Asset class, either it's already a
// string asset id
return expandAsset(obj[field].toString());
});
}

const ids: AssetID[] = source.map((s: any) => s[field].toString());

return db.assets.findAllByIDs(ids);
});

function expandAsset(assetId: AssetID) {
if (assetId === "native") {
return { id: "native", code: "XLM", issuer: null, native: true };
}

const [code, issuer] = assetId.split("-");
return { id: assetId, code, issuer, native: false };
}
7 changes: 4 additions & 3 deletions src/schema/resolvers/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ export function idOnlyRequested(info: any): boolean {
return false;
}

export function onlyFieldsRequested(info: any, fields: string[]): boolean {
const requestedFields = [...new Set(fieldsList(info))]; // dedupe
// Returns true, iff user didn't request any fields, except those listed in `fields` parameter
export function onlyFieldsRequested(info: any, ...fields: string[]): boolean {
const difference = fieldsList(info).filter(f => !fields.includes(f));

return JSON.stringify(requestedFields.sort()) === JSON.stringify(fields.sort());
return difference.length === 0;
}

export function makeConnection<T extends IWithPagingToken, R = T>(records: T[], nodeBuilder?: (r: T) => R) {
Expand Down
25 changes: 25 additions & 0 deletions src/service/dex/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { BigNumber } from "bignumber.js";
import { AssetID } from "../../model";
import { Offer } from "../../orm/entities/offer";
import { OffersGraph } from "./offers_graph";
import { load as loadOrderBook } from "./orderbook";
import { PathFinder } from "./path_finder";

const offersGraph = new OffersGraph();

export * from "./offers_listener";

export function buildOffersGraph(offers: Offer[]): void {
offersGraph.build(offers);
}

export function updateOffersGraph(selling: AssetID, buying: AssetID, offers: Offer[]): void {
offersGraph.update(selling, buying, offers);
}

export function findPaymentPaths(sourceAssets: AssetID[], destAsset: AssetID, destAmount: BigNumber) {
const pathFinder = new PathFinder(offersGraph);
return pathFinder.findPaths(sourceAssets, destAsset, destAmount);
}

export const orderBook = { load: loadOrderBook };
Loading