Skip to content

Commit 6717a86

Browse files
committed
solution: api for requesting the historical market prices
1 parent 3386926 commit 6717a86

6 files changed

Lines changed: 162 additions & 16 deletions

File tree

api-definitions

packages/core/src/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,16 @@ export {
8080
} from './typesCommon';
8181
export { MessageFactory } from './typesConvert';
8282
export {
83+
CryptoCurrency,
84+
StablecoinCurrency,
85+
CountryCurrency,
86+
BaseCurrency,
8387
AnyCurrency,
8488
ConvertMarket,
85-
CountryCurrency,
86-
CryptoCurrency,
8789
GetRatesRequest,
8890
GetRatesResponse,
8991
Pair,
9092
Rate,
91-
StablecoinCurrency,
9293
TestCurrency,
9394
} from './typesMarket';
9495
export * as token from './typesToken';

packages/core/src/typesMarket.ts

Lines changed: 114 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import * as market_pb from './generated/market_pb';
2+
import * as common_pb from './generated/common_pb';
23
import { DataMapper } from './Publisher';
3-
import { ConvertCommon, Erc20Asset, isErc20Asset } from './typesCommon';
4+
import { Blockchain, ConvertCommon, Erc20Asset, isErc20Asset } from './typesCommon';
45
import { MessageFactory } from './typesConvert';
56

6-
export type CryptoCurrency = 'BTC' | 'GRIN' | 'ETC' | 'ETH';
7-
export type StablecoinCurrency = 'DAI' | 'SAI' | 'USDT';
7+
export type CryptoCurrency = 'BTC' | 'ETC' | 'ETH';
8+
export type StablecoinCurrency = 'DAI' | 'USDT' | 'USDC';
89
export type CountryCurrency =
910
| 'USD'
1011
| 'EUR'
@@ -20,29 +21,95 @@ export type CountryCurrency =
2021
| 'KRW'
2122
| 'SGD'
2223
| 'INR';
23-
export type TestCurrency = 'MONOPOLY' | 'KOVAN' | 'TEST_BTC';
24+
export type TestCurrency = 'MONOPOLY' | 'KOVAN' | 'TEST_BTC' | 'TEST_BTC_V4' | 'SEPOLIA';
2425

2526
export type BaseCurrency = CryptoCurrency | StablecoinCurrency | CountryCurrency | TestCurrency;
2627
export type AnyCurrency = BaseCurrency | Erc20Asset;
2728

28-
export type GetRatesRequest = Pair[];
2929

30+
/**
31+
* Currency pair
32+
*/
3033
export type Pair = {
3134
base: AnyCurrency;
3235
target: AnyCurrency;
3336
};
3437

35-
export type GetRatesResponse = Rate[];
38+
/**
39+
* Request the rate at specified moment.
40+
*/
41+
export type GetRatesRequestAt = {
42+
// Request the rate at the specified moment.
43+
timestamp: number | Date;
44+
// The rates
45+
pairs: Pair[];
46+
}
47+
48+
/**
49+
* Request the rate at the moment of the specified block.
50+
*/
51+
export type GetRatesRequestByBlock = {
52+
// Request the rate at the moment of the specified block.
53+
block: BlockRefAtHeight | BlockRefAtHash;
54+
// The rates
55+
pairs: Pair[];
56+
}
57+
58+
/**
59+
* Reference to a block by its height.
60+
*/
61+
export type BlockRefAtHeight = {
62+
// The blockchain to which the block belongs.
63+
blockchain: Blockchain,
64+
// The height of the block.
65+
height: number,
66+
}
67+
68+
/**
69+
* Reference to a block by its hash.
70+
*/
71+
export type BlockRefAtHash = {
72+
// The blockchain to which the block belongs.
73+
blockchain: Blockchain,
74+
// The hash of the block.
75+
hash: string
76+
}
77+
78+
/**
79+
* Request the latest available rate.
80+
* Note, there rates are averaged from multiple exchanges and it always a slight delay to fetch the trades.
81+
* Because of that, when requesting the latest rate, it may fluctuate a little between the requests.
82+
*/
83+
export type GetRatesRequestLatest = Pair[] | { pairs: Pair[] }
84+
85+
/**
86+
* Reqeust the rates. It can be either the latest available rates, or the rates at the specified moment (timestamp or block).
87+
*/
88+
export type GetRatesRequest = GetRatesRequestLatest | GetRatesRequestAt | GetRatesRequestByBlock;
89+
3690

91+
/**
92+
* Currency pair with the exchange rate.
93+
*/
3794
export type Rate = {
3895
base: AnyCurrency;
3996
target: AnyCurrency;
4097
/**
4198
* exchange rate decimal encoded as a string
4299
*/
43100
rate: string;
101+
102+
/**
103+
* Timestamp of the moment when the rate was established.
104+
* It points to the moment when the last time one of the components of the pair was traded, which affected the rate.
105+
* It's assumed that if there were no trades between that moment, and the moment of the request then the rate is still valid.
106+
* I.e., it's the last known evidence of the rate. If multiple pairs requested, the timestamp of each pair may be different.
107+
*/
108+
timestamp?: Date;
44109
};
45110

111+
export type GetRatesResponse = Rate[];
112+
46113
export class ConvertMarket {
47114
private readonly factory: MessageFactory;
48115

@@ -53,10 +120,22 @@ export class ConvertMarket {
53120
this.common = common;
54121
}
55122

56-
public ratesRequest(pairs: GetRatesRequest): market_pb.GetRatesRequest {
123+
/**
124+
* Convert TypeScript type to Protobuf message
125+
* @param request
126+
*/
127+
public ratesRequest(request: GetRatesRequest): market_pb.GetRatesRequest {
57128
const req: market_pb.GetRatesRequest = this.factory('market_pb.GetRatesRequest');
58129

59-
pairs.forEach((pair) => {
130+
let pairsList: Pair[];
131+
132+
if (Array.isArray(request)) {
133+
pairsList = request;
134+
} else if ('pairs' in request) {
135+
pairsList = request.pairs;
136+
}
137+
138+
pairsList.forEach((pair) => {
60139
const pairProto: market_pb.Pair = this.factory('market_pb.Pair');
61140

62141
if (isErc20Asset(pair.base)) {
@@ -73,9 +152,27 @@ export class ConvertMarket {
73152

74153
req.addPairs(pairProto);
75154
});
155+
156+
if ('timestamp' in request) {
157+
req.setTimestamp(request.timestamp instanceof Date ? request.timestamp.getTime() : request.timestamp);
158+
} else if ('block' in request) {
159+
const blockRef = request.block;
160+
const blockRefProto: common_pb.BlockRef = this.factory('common_pb.BlockRef');
161+
blockRefProto.setBlockchain(blockRef.blockchain.valueOf());
162+
if ('height' in blockRef) {
163+
blockRefProto.setHeight(blockRef.height);
164+
} else if ('hash' in blockRef) {
165+
blockRefProto.setBlockId(blockRef.hash);
166+
}
167+
req.setBlock(blockRefProto);
168+
}
169+
76170
return req;
77171
}
78172

173+
/**
174+
* Convert Protobuf message to TypeScript type
175+
*/
79176
public ratesResponse(): DataMapper<market_pb.GetRatesResponse, GetRatesResponse> {
80177
return (value) => {
81178
const rates: Rate[] = value.getRatesList().map((rate) => {
@@ -89,19 +186,25 @@ export class ConvertMarket {
89186
throw new Error('No base currency in rate');
90187
}
91188

92-
let target;
189+
let target: AnyCurrency;
93190

94191
if (rate.hasErc20Target()) {
95192
target = this.common.erc20Asset(rate.getErc20Target());
96193
} else if (rate.hasTarget()) {
97-
target = rate.getTarget();
194+
target = rate.getTarget() as BaseCurrency;
195+
}
196+
197+
let timestamp: Date | undefined = undefined;
198+
if (rate.getTimestamp() > 0) {
199+
timestamp = new Date(rate.getTimestamp());
98200
}
99201

100202
return {
101203
base: base,
102204
target: target,
103205
rate: rate.getRate(),
104-
} as Rate;
206+
timestamp: timestamp,
207+
}
105208
});
106209

107210
return rates;

packages/node/src/wrapped/Factory.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export const classFactory: MessageFactory = (id: string) => {
6161
return new market_pb.GetRatesRequest();
6262
case 'market_pb.Pair':
6363
return new market_pb.Pair();
64+
case 'common_pb.BlockRef':
65+
return new common_pb.BlockRef();
6466
// Token
6567
case 'token_message_pb.AddressAllowanceRequest':
6668
return new token_message_pb.AddressAllowanceRequest();

packages/web/src/__integration-tests__/market.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import {EmeraldApi} from "../EmeraldApi";
1+
import { EmeraldApi } from "../EmeraldApi";
2+
import { Blockchain } from "@emeraldpay/api";
23

34
jest.setTimeout(5000);
45

@@ -21,6 +22,41 @@ describe("MarketClient", () => {
2122
expect(parseFloat(act[0].rate)).toBeGreaterThan(50);
2223
});
2324

25+
test('Get ETH rate at block 24319623', async () => {
26+
const client = api.market;
27+
28+
let act = await client.getRates({
29+
block: {
30+
blockchain: Blockchain.ETHEREUM,
31+
height: 24319623,
32+
},
33+
pairs: [{base: "ETH", target: "USD"}],
34+
});
35+
console.log("rates", act);
36+
37+
expect(act.length).toBe(1);
38+
expect(act[0].base).toBe("ETH");
39+
expect(act[0].target).toBe("USD");
40+
expect(parseFloat(act[0].rate)).toBeGreaterThan(2850.0);
41+
expect(parseFloat(act[0].rate)).toBeLessThan(2950.0);
42+
});
43+
44+
test('Get ETH rate at 6 Feb 2026', async () => {
45+
const client = api.market;
46+
47+
let act = await client.getRates({
48+
timestamp: new Date('2026-02-06T12:00:00Z'),
49+
pairs: [{base: "ETH", target: "USD"}],
50+
});
51+
console.log("rates", act);
52+
53+
expect(act.length).toBe(1);
54+
expect(act[0].base).toBe("ETH");
55+
expect(act[0].target).toBe("USD");
56+
expect(parseFloat(act[0].rate)).toBeGreaterThan(1900.0);
57+
expect(parseFloat(act[0].rate)).toBeLessThan(2000.0);
58+
});
59+
2460
test('Get BTC rate', async () => {
2561
const client = api.market;
2662

packages/web/src/wrapped/Factory.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ export const classFactory: MessageFactory = (id: string) => {
7575
if (id == "market_pb.Pair") {
7676
return new market_pb.Pair();
7777
}
78+
if (id == "common_pb.BlockRef") {
79+
return new common_pb.BlockRef();
80+
}
81+
7882
// Sierra
7983
if (id == "sierra_message_pb.CreateProjectRequest") {
8084
return new sierra_message_pb.CreateProjectRequest();

0 commit comments

Comments
 (0)