Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import type {
KeetaAssetMovementClientConfig
} from '../services/asset-movement/client.ts';
import KeetaAssetMovementAnchorClient from '../services/asset-movement/client.js';
import type {
KeetaOrderMatcherClientConfig
} from '../services/order-matcher/client.ts';
import KeetaOrderMatcherClient from '../services/order-matcher/client.js';

// TODO: Determine how we want to export the client
// eslint-disable-next-line @typescript-eslint/no-namespace
Expand All @@ -32,6 +36,13 @@ export namespace AssetMovement {
export const Client: typeof KeetaAssetMovementAnchorClient = KeetaAssetMovementAnchorClient;
}

// TODO: Determine how we want to export the client
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace OrderMatcher {
export type ClientConfig = KeetaOrderMatcherClientConfig;
export const Client: typeof KeetaOrderMatcherClient = KeetaOrderMatcherClient;
}

export {
lib,
KeetaNet
Expand Down
196 changes: 195 additions & 1 deletion src/lib/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as KeetaNetClient from '@keetanetwork/keetanet-client';
import type { GenericAccount as KeetaNetGenericAccount } from '@keetanetwork/keetanet-client/lib/account.js';
import type { GenericAccount as KeetaNetGenericAccount, TokenAddress } from '@keetanetwork/keetanet-client/lib/account.js';
import * as CurrencyInfo from '@keetanetwork/currency-info';
import type { Logger } from './log/index.ts';
import type { JSONSerializable } from './utils/json.ts';
Expand All @@ -16,6 +16,7 @@ type ExternalURL = { external: '2b828e33-2692-46e9-817e-9b93d63f28fd'; url: stri
type KeetaNetAccount = InstanceType<typeof KeetaNetClient.lib.Account>;
const KeetaNetAccount: typeof KeetaNetClient.lib.Account = KeetaNetClient.lib.Account;
type KeetaNetAccountTokenPublicKeyString = ReturnType<InstanceType<typeof KeetaNetClient.lib.Account<typeof KeetaNetAccount.AccountKeyAlgorithm.TOKEN>>['publicKeyString']['get']>;
type KeetaNetAccountPublicKeyString = ReturnType<InstanceType<typeof KeetaNetClient.lib.Account>['publicKeyString']['get']>;

/**
* Canonical form of a currency code for use in the ServiceMetadata
Expand Down Expand Up @@ -162,6 +163,75 @@ type ServiceMetadata = {
}[];
}
};
/**
* Order Matcher services
*
* This is used to identify service providers which perform MATCH_SWAP operations on the network, and the pairs that they support
*/
orderMatcher?: {
/**
* Provider ID which identifies the Order Matcher provider
*/
[id: string]: {
operations: {
/**
* Path for retrieving price history for a token pair
*/
getPairHistory?: string;

/**
* Path for retrieving price info/basic stats for a token pair
*/
getPairInfo?: string;

/**
* Path for retrieving order book depth for a token pair, with a specified grouping
*/
getPairDepth?: string;
};

/**
* A list of accounts which this Order Matcher can sign MATCH_SWAP blocks with
* The user must grant permission to these accounts in order for the order matcher to be able to perform matches
*/
matchingAccounts: KeetaNetAccountPublicKeyString[];

/**
* Path for which can be used to identify which
* tokens this order matcher provider will operate on
*/
pairs: {
/**
* Base token(s) for the pair
* These will be combined with each quote token to form trading pairs
*/
base: KeetaNetAccountTokenPublicKeyString[];

/**
* Quote token(s) for the pair
* These will be combined with each base token to form trading pairs
*/
quote: KeetaNetAccountTokenPublicKeyString[];

/**
* Fees structure for this trading pair (if any)
* The provider can choose to not accept orders for this pair if the fee structure is not met
*/
fees?: {
/**
* Type of fee structure applied to this trading pair
* "sell-token-percentage" means that a percentage of the sold token's amount must be provided as a fee
*/
type: 'sell-token-percentage';

/**
* The minimum percentage fee (in basis points) that must be given when creating swaps for this pair
*/
minPercentBasisPoints: number;
}
}[];
}
};
assetMovement?: {
[id: string]: {
operations: {
Expand Down Expand Up @@ -246,6 +316,17 @@ type ServiceSearchCriteria<T extends Services> = {
*/
kycProviders?: string[];
};
'orderMatcher': {
/**
* Search for providers which support the base token
*/
base?: KeetaNetAccountTokenPublicKeyString;

/**
* Search for providers which support the specified quote token
*/
quote?: KeetaNetAccountTokenPublicKeyString;
};
'kyc': {
/**
* Search for a KYC provider which can verify accounts in ALL
Expand Down Expand Up @@ -374,6 +455,11 @@ function assertValidOperationsFX(input: unknown): asserts input is { operations:
assertValidOperationsBanking(input);
}

function assertValidOperationsOrderMatcher(input: unknown): asserts input is { operations: ToValuizableObject<NonNullable<ServiceMetadata['services']['orderMatcher']>[string]>['operations'] } {
/* XXX:TODO: Validate the specific operations */
assertValidOperationsBanking(input);
}

function assertValidOperationsAssetMovement(input: unknown): asserts input is { operations: ToValuizableObject<NonNullable<ServiceMetadata['services']['assetMovement']>[string]>['operations'] } {
/* XXX:TODO: Validate the specific operations */
assertValidOperationsBanking(input);
Expand Down Expand Up @@ -451,6 +537,23 @@ const assertResolverLookupFXResult = async function(input: unknown): Promise<Res
return(input);
};

const assertResolverLookupOrderMatcherResult = async function(input: unknown): Promise<ResolverLookupServiceResults<'orderMatcher'>[string]> {
assertValidOperationsOrderMatcher(input);
if (!('pairs' in input)) {
throw(new Error('Expected "pairs" key in order matcher service, but it was not found'));
}

const pairsUnrealized = input.pairs;
// eslint-disable-next-line @typescript-eslint/no-use-before-define
if (!Metadata.isValuizable(pairsUnrealized)) {
throw(new Error(`Expected "pairs" to be an Valuizable, got ${typeof pairsUnrealized}`));
}
// XXX:TODO: Perform deeper validation of the "pairs" structure
await pairsUnrealized('array');
// @ts-ignore
return(input);
}

const assertResolverLookupAssetMovementResults = async function(input: unknown): Promise<ResolverLookupServiceResults<'assetMovement'>[string]> {
assertValidOperationsAssetMovement(input);
// assertValidOperationsKYC(input);
Expand Down Expand Up @@ -1276,6 +1379,9 @@ class Resolver {
'fx': {
search: this.lookupFXServices.bind(this)
},
'orderMatcher': {
search: this.lookupOrderMatcherServices.bind(this)
},
'assetMovement': {
search: this.lookupAssetMovementServices.bind(this)
},
Expand Down Expand Up @@ -1547,6 +1653,94 @@ class Resolver {
return(retval);
}

private async lookupOrderMatcherServices(orderMatcherServices: ValuizableObject | undefined, criteria: ServiceSearchCriteria<'orderMatcher'>) {
if (orderMatcherServices === undefined) {
return(undefined);
}

const retval: ResolverLookupServiceResults<'orderMatcher'> = {};
for (const checkOrderMatcherServiceID in orderMatcherServices) {
try {
const checkOrderMatcherService = await assertResolverLookupOrderMatcherResult(await orderMatcherServices[checkOrderMatcherServiceID]?.('object'));
if (!checkOrderMatcherService) {
continue;
}

const pairsUnrealized: ToValuizable<NonNullable<ServiceMetadata['services']['orderMatcher']>[string]['pairs']> = checkOrderMatcherService.pairs;
const pairs = await pairsUnrealized?.('array');
if (pairs === undefined) {
continue;
}

let acceptable = false;

if (criteria.base === undefined && criteria.quote === undefined) {
acceptable = true;
} else {
for (const pairUnrealized of pairs) {
const pair = await pairUnrealized?.('object');
if (pair === undefined) {
continue;
}

const [ baseTokenAddresses, quoteTokenAddresses ] = await Promise.all([ pair.base, pair.quote ].map(async (tokenListUnrealized) => {
const tokenList = await tokenListUnrealized?.('array') ?? [];

const resolvedAddresses = await Promise.all(tokenList.map(async (tokenUnrealized) => {
try {
const publicKey = await tokenUnrealized?.('string');
return(KeetaNetAccount.fromPublicKeyString(publicKey).assertKeyType(KeetaNetAccount.AccountKeyAlgorithm.TOKEN));
} catch (error) {
this.#logger?.debug(`Resolver:${this.id}`, 'Error resolving token address in order matcher pair:', error, ' -- ignoring');
return(undefined);
}
}));

const tokenAddresses = resolvedAddresses.filter(function(token): token is TokenAddress {
return(token !== undefined);
});

return(new KeetaNetAccount.Set(tokenAddresses));
}));

if (!baseTokenAddresses || !quoteTokenAddresses) {
throw(new Error('internal error: base or quote token addresses could not be resolved'));
}

if (criteria.base !== undefined && !baseTokenAddresses.has(KeetaNetAccount.fromPublicKeyString(criteria.base))) {
continue;
}

if (criteria.quote !== undefined && !quoteTokenAddresses.has(KeetaNetAccount.fromPublicKeyString(criteria.quote))) {
continue;
}

acceptable = true;
break;
}
}

if (!acceptable) {
continue;
}

retval[checkOrderMatcherServiceID] = checkOrderMatcherService;
} catch (checkOrderMatcherServiceError) {
this.#logger?.debug(`Resolver:${this.id}`, 'Error checking Order Matcher service', checkOrderMatcherServiceID, ':', checkOrderMatcherServiceError, ' -- ignoring');
}
}

if (Object.keys(retval).length === 0) {
/*
* If we didn't find any order matcher services, then we return
* undefined to indicate that no services were found.
*/
return(undefined);
}

return(retval);
}

async filterSupportedAssets(assetService: ValuizableObject, criteria: ServiceSearchCriteria<'assetMovement'> = {}): Promise<SupportedAssets[]> {
const assetCanonical = criteria.asset ? convertAssetOrPairSearchInputToCanonical(criteria.asset) : undefined;
const fromCanonical = criteria.from ? convertAssetLocationInputToCanonical(criteria.from) : undefined;
Expand Down
Loading