Skip to content

Commit

Permalink
Implementing path finding without Horizon
Browse files Browse the repository at this point in the history
  • Loading branch information
charlie-wasp authored Jun 18, 2019
1 parent d5c856e commit f1230cf
Show file tree
Hide file tree
Showing 20 changed files with 512 additions and 101 deletions.
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

0 comments on commit f1230cf

Please sign in to comment.