@ledgerhq/coin-hedera 1.16.0-nightly.20251212024049 → 1.16.0-nightly.20251213023821
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -10
- package/lib/api/index.d.ts.map +1 -1
- package/lib/api/index.js +4 -3
- package/lib/api/index.js.map +1 -1
- package/lib/bridge/buildOptimisticOperation.d.ts.map +1 -1
- package/lib/bridge/buildOptimisticOperation.js +14 -43
- package/lib/bridge/buildOptimisticOperation.js.map +1 -1
- package/lib/constants.d.ts +11 -6
- package/lib/constants.d.ts.map +1 -1
- package/lib/constants.js +20 -1
- package/lib/constants.js.map +1 -1
- package/lib/deviceTransactionConfig.d.ts.map +1 -1
- package/lib/deviceTransactionConfig.js +1 -3
- package/lib/deviceTransactionConfig.js.map +1 -1
- package/lib/logic/getBalance.d.ts.map +1 -1
- package/lib/logic/getBalance.js +21 -4
- package/lib/logic/getBalance.js.map +1 -1
- package/lib/logic/getBlock.d.ts.map +1 -1
- package/lib/logic/getBlock.js +39 -2
- package/lib/logic/getBlock.js.map +1 -1
- package/lib/logic/getValidators.d.ts +3 -0
- package/lib/logic/getValidators.d.ts.map +1 -0
- package/lib/logic/getValidators.js +24 -0
- package/lib/logic/getValidators.js.map +1 -0
- package/lib/logic/index.d.ts +1 -0
- package/lib/logic/index.d.ts.map +1 -1
- package/lib/logic/index.js +3 -1
- package/lib/logic/index.js.map +1 -1
- package/lib/logic/listOperations.d.ts.map +1 -1
- package/lib/logic/listOperations.js +16 -2
- package/lib/logic/listOperations.js.map +1 -1
- package/lib/logic/utils.d.ts +17 -1
- package/lib/logic/utils.d.ts.map +1 -1
- package/lib/logic/utils.js +54 -1
- package/lib/logic/utils.js.map +1 -1
- package/lib/network/api.d.ts +22 -2
- package/lib/network/api.d.ts.map +1 -1
- package/lib/network/api.js +49 -14
- package/lib/network/api.js.map +1 -1
- package/lib/preload.js +2 -2
- package/lib/preload.js.map +1 -1
- package/lib/test/fixtures/account.fixture.d.ts +8 -0
- package/lib/test/fixtures/account.fixture.d.ts.map +1 -1
- package/lib/test/fixtures/account.fixture.js +8 -0
- package/lib/test/fixtures/account.fixture.js.map +1 -1
- package/lib/test/fixtures/mirror.fixture.d.ts +2 -1
- package/lib/test/fixtures/mirror.fixture.d.ts.map +1 -1
- package/lib/test/fixtures/mirror.fixture.js +16 -1
- package/lib/test/fixtures/mirror.fixture.js.map +1 -1
- package/lib/types/bridge.d.ts +1 -0
- package/lib/types/bridge.d.ts.map +1 -1
- package/lib/types/logic.d.ts +6 -0
- package/lib/types/logic.d.ts.map +1 -1
- package/lib-es/api/index.d.ts.map +1 -1
- package/lib-es/api/index.js +5 -4
- package/lib-es/api/index.js.map +1 -1
- package/lib-es/bridge/buildOptimisticOperation.d.ts.map +1 -1
- package/lib-es/bridge/buildOptimisticOperation.js +15 -44
- package/lib-es/bridge/buildOptimisticOperation.js.map +1 -1
- package/lib-es/constants.d.ts +11 -6
- package/lib-es/constants.d.ts.map +1 -1
- package/lib-es/constants.js +19 -0
- package/lib-es/constants.js.map +1 -1
- package/lib-es/deviceTransactionConfig.d.ts.map +1 -1
- package/lib-es/deviceTransactionConfig.js +2 -4
- package/lib-es/deviceTransactionConfig.js.map +1 -1
- package/lib-es/logic/getBalance.d.ts.map +1 -1
- package/lib-es/logic/getBalance.js +21 -4
- package/lib-es/logic/getBalance.js.map +1 -1
- package/lib-es/logic/getBlock.d.ts.map +1 -1
- package/lib-es/logic/getBlock.js +40 -3
- package/lib-es/logic/getBlock.js.map +1 -1
- package/lib-es/logic/getValidators.d.ts +3 -0
- package/lib-es/logic/getValidators.d.ts.map +1 -0
- package/lib-es/logic/getValidators.js +20 -0
- package/lib-es/logic/getValidators.js.map +1 -0
- package/lib-es/logic/index.d.ts +1 -0
- package/lib-es/logic/index.d.ts.map +1 -1
- package/lib-es/logic/index.js +1 -0
- package/lib-es/logic/index.js.map +1 -1
- package/lib-es/logic/listOperations.d.ts.map +1 -1
- package/lib-es/logic/listOperations.js +17 -3
- package/lib-es/logic/listOperations.js.map +1 -1
- package/lib-es/logic/utils.d.ts +17 -1
- package/lib-es/logic/utils.d.ts.map +1 -1
- package/lib-es/logic/utils.js +52 -1
- package/lib-es/logic/utils.js.map +1 -1
- package/lib-es/network/api.d.ts +22 -2
- package/lib-es/network/api.d.ts.map +1 -1
- package/lib-es/network/api.js +49 -14
- package/lib-es/network/api.js.map +1 -1
- package/lib-es/preload.js +2 -2
- package/lib-es/preload.js.map +1 -1
- package/lib-es/test/fixtures/account.fixture.d.ts +8 -0
- package/lib-es/test/fixtures/account.fixture.d.ts.map +1 -1
- package/lib-es/test/fixtures/account.fixture.js +8 -0
- package/lib-es/test/fixtures/account.fixture.js.map +1 -1
- package/lib-es/test/fixtures/mirror.fixture.d.ts +2 -1
- package/lib-es/test/fixtures/mirror.fixture.d.ts.map +1 -1
- package/lib-es/test/fixtures/mirror.fixture.js +14 -0
- package/lib-es/test/fixtures/mirror.fixture.js.map +1 -1
- package/lib-es/types/bridge.d.ts +1 -0
- package/lib-es/types/bridge.d.ts.map +1 -1
- package/lib-es/types/logic.d.ts +6 -0
- package/lib-es/types/logic.d.ts.map +1 -1
- package/package.json +10 -10
- package/src/api/index.integ.test.ts +226 -1
- package/src/api/index.test.ts +5 -2
- package/src/api/index.ts +5 -5
- package/src/bridge/{buildOptimisticOperation.integration.test.ts → buildOptimisticOperation.test.ts} +23 -68
- package/src/bridge/buildOptimisticOperation.ts +16 -45
- package/src/constants.ts +23 -1
- package/src/deviceTransactionConfig.test.ts +59 -43
- package/src/deviceTransactionConfig.ts +2 -5
- package/src/logic/getBalance.test.ts +50 -0
- package/src/logic/getBalance.ts +21 -4
- package/src/logic/getBlock.test.ts +283 -1
- package/src/logic/getBlock.ts +57 -6
- package/src/logic/getValidators.test.ts +50 -0
- package/src/logic/getValidators.ts +22 -0
- package/src/logic/index.ts +1 -0
- package/src/logic/listOperations.ts +33 -3
- package/src/logic/utils.test.ts +113 -0
- package/src/logic/utils.ts +67 -1
- package/src/network/api.test.ts +55 -9
- package/src/network/api.ts +66 -14
- package/src/preload.ts +2 -2
- package/src/test/fixtures/account.fixture.ts +8 -0
- package/src/test/fixtures/mirror.fixture.ts +18 -0
- package/src/types/bridge.ts +1 -0
- package/src/types/logic.ts +7 -0
|
@@ -22,6 +22,7 @@ describe("getBalance", () => {
|
|
|
22
22
|
|
|
23
23
|
(apiClient.getAccount as jest.Mock).mockResolvedValue(mockMirrorAccount);
|
|
24
24
|
(apiClient.getAccountTokens as jest.Mock).mockResolvedValue([]);
|
|
25
|
+
(apiClient.getNodes as jest.Mock).mockResolvedValue({ nodes: [] });
|
|
25
26
|
|
|
26
27
|
const result = await getBalance(mockCurrency, address);
|
|
27
28
|
|
|
@@ -70,6 +71,7 @@ describe("getBalance", () => {
|
|
|
70
71
|
|
|
71
72
|
(apiClient.getAccount as jest.Mock).mockResolvedValue(mockMirrorAccount);
|
|
72
73
|
(apiClient.getAccountTokens as jest.Mock).mockResolvedValue(mockMirrorTokens);
|
|
74
|
+
(apiClient.getNodes as jest.Mock).mockResolvedValue({ nodes: [] });
|
|
73
75
|
|
|
74
76
|
const result = await getBalance(mockCurrency, address);
|
|
75
77
|
|
|
@@ -99,6 +101,51 @@ describe("getBalance", () => {
|
|
|
99
101
|
);
|
|
100
102
|
});
|
|
101
103
|
|
|
104
|
+
it("should return stake", async () => {
|
|
105
|
+
const address = "0.0.12345";
|
|
106
|
+
const mockCurrency = getMockedCurrency();
|
|
107
|
+
const mockMirrorAccount = {
|
|
108
|
+
account: address,
|
|
109
|
+
staked_node_id: 5,
|
|
110
|
+
balance: {
|
|
111
|
+
balance: 100,
|
|
112
|
+
},
|
|
113
|
+
pending_reward: 100,
|
|
114
|
+
};
|
|
115
|
+
const mockMirrorNode = {
|
|
116
|
+
node_id: 5,
|
|
117
|
+
node_account_id: "0.0.5",
|
|
118
|
+
description: "Hosted for Wipro | Amsterdam, Netherlands",
|
|
119
|
+
max_stake: 45000000000000000,
|
|
120
|
+
stake: 45000000000000000,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
(apiClient.getAccount as jest.Mock).mockResolvedValue(mockMirrorAccount);
|
|
124
|
+
(apiClient.getAccountTokens as jest.Mock).mockResolvedValue([]);
|
|
125
|
+
(apiClient.getNodes as jest.Mock).mockResolvedValue({ nodes: [mockMirrorNode] });
|
|
126
|
+
|
|
127
|
+
const result = await getBalance(mockCurrency, address);
|
|
128
|
+
|
|
129
|
+
expect(apiClient.getAccount).toHaveBeenCalledTimes(1);
|
|
130
|
+
expect(apiClient.getAccount).toHaveBeenCalledWith(address);
|
|
131
|
+
expect(apiClient.getNodes).toHaveBeenCalledTimes(1);
|
|
132
|
+
expect(result).toHaveLength(1);
|
|
133
|
+
expect(result[0]).toMatchObject({
|
|
134
|
+
asset: { type: "native" },
|
|
135
|
+
value: BigInt(mockMirrorAccount.balance.balance),
|
|
136
|
+
stake: {
|
|
137
|
+
uid: address,
|
|
138
|
+
address,
|
|
139
|
+
asset: { type: "native" },
|
|
140
|
+
state: "active",
|
|
141
|
+
amount: BigInt(mockMirrorAccount.balance.balance + mockMirrorAccount.pending_reward),
|
|
142
|
+
amountDeposited: BigInt(mockMirrorAccount.balance.balance),
|
|
143
|
+
amountRewarded: BigInt(mockMirrorAccount.pending_reward),
|
|
144
|
+
delegate: mockMirrorNode.node_account_id,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
102
149
|
it("should skip tokens not found in CAL", async () => {
|
|
103
150
|
const address = "0.0.12345";
|
|
104
151
|
const mockCurrency = getMockedCurrency();
|
|
@@ -145,6 +192,7 @@ describe("getBalance", () => {
|
|
|
145
192
|
|
|
146
193
|
(apiClient.getAccount as jest.Mock).mockResolvedValue(mockMirrorAccount);
|
|
147
194
|
(apiClient.getAccountTokens as jest.Mock).mockResolvedValue(mockMirrorTokens);
|
|
195
|
+
(apiClient.getNodes as jest.Mock).mockResolvedValue({ nodes: [] });
|
|
148
196
|
|
|
149
197
|
const result = await getBalance(mockCurrency, address);
|
|
150
198
|
|
|
@@ -173,6 +221,7 @@ describe("getBalance", () => {
|
|
|
173
221
|
|
|
174
222
|
(apiClient.getAccount as jest.Mock).mockRejectedValue(error);
|
|
175
223
|
(apiClient.getAccountTokens as jest.Mock).mockResolvedValue([]);
|
|
224
|
+
(apiClient.getNodes as jest.Mock).mockResolvedValue({ nodes: [] });
|
|
176
225
|
|
|
177
226
|
await expect(getBalance(mockCurrency, address)).rejects.toThrow(error);
|
|
178
227
|
});
|
|
@@ -189,6 +238,7 @@ describe("getBalance", () => {
|
|
|
189
238
|
|
|
190
239
|
(apiClient.getAccount as jest.Mock).mockResolvedValue(mockMirrorAccount);
|
|
191
240
|
(apiClient.getAccountTokens as jest.Mock).mockRejectedValue(error);
|
|
241
|
+
(apiClient.getNodes as jest.Mock).mockResolvedValue({ nodes: [] });
|
|
192
242
|
|
|
193
243
|
await expect(getBalance(mockCurrency, address)).rejects.toThrow(error);
|
|
194
244
|
});
|
package/src/logic/getBalance.ts
CHANGED
|
@@ -4,15 +4,32 @@ import { getCryptoAssetsStore } from "@ledgerhq/cryptoassets/state";
|
|
|
4
4
|
import { apiClient } from "../network/api";
|
|
5
5
|
|
|
6
6
|
export async function getBalance(currency: CryptoCurrency, address: string): Promise<Balance[]> {
|
|
7
|
-
const [mirrorAccount, mirrorTokens] = await Promise.all([
|
|
7
|
+
const [mirrorAccount, mirrorTokens, mirrorNodes] = await Promise.all([
|
|
8
8
|
apiClient.getAccount(address),
|
|
9
9
|
apiClient.getAccountTokens(address),
|
|
10
|
+
apiClient.getNodes({ fetchAllPages: true }),
|
|
10
11
|
]);
|
|
11
12
|
|
|
12
|
-
const
|
|
13
|
+
const validator = mirrorNodes.nodes.find(v => v.node_id === mirrorAccount.staked_node_id);
|
|
14
|
+
const balances: Balance[] = [
|
|
13
15
|
{
|
|
14
16
|
asset: { type: "native" },
|
|
15
17
|
value: BigInt(mirrorAccount.balance.balance),
|
|
18
|
+
...(validator && {
|
|
19
|
+
stake: {
|
|
20
|
+
uid: address,
|
|
21
|
+
address,
|
|
22
|
+
asset: { type: "native" },
|
|
23
|
+
state: "active",
|
|
24
|
+
amount: BigInt(mirrorAccount.balance.balance) + BigInt(mirrorAccount.pending_reward),
|
|
25
|
+
amountDeposited: BigInt(mirrorAccount.balance.balance),
|
|
26
|
+
amountRewarded: BigInt(mirrorAccount.pending_reward),
|
|
27
|
+
delegate: validator.node_account_id,
|
|
28
|
+
details: {
|
|
29
|
+
overstaked: BigInt(validator.stake) >= BigInt(validator.max_stake),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
16
33
|
},
|
|
17
34
|
];
|
|
18
35
|
|
|
@@ -26,7 +43,7 @@ export async function getBalance(currency: CryptoCurrency, address: string): Pro
|
|
|
26
43
|
continue;
|
|
27
44
|
}
|
|
28
45
|
|
|
29
|
-
|
|
46
|
+
balances.push({
|
|
30
47
|
value: BigInt(mirrorToken.balance),
|
|
31
48
|
asset: {
|
|
32
49
|
type: calToken.tokenType,
|
|
@@ -38,5 +55,5 @@ export async function getBalance(currency: CryptoCurrency, address: string): Pro
|
|
|
38
55
|
});
|
|
39
56
|
}
|
|
40
57
|
|
|
41
|
-
return
|
|
58
|
+
return balances;
|
|
42
59
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { HEDERA_TRANSACTION_NAMES } from "../constants";
|
|
1
2
|
import { getBlock } from "./getBlock";
|
|
2
3
|
import { getBlockInfo } from "./getBlockInfo";
|
|
3
4
|
import { apiClient } from "../network/api";
|
|
4
|
-
import {
|
|
5
|
+
import type { StakingAnalysis } from "../types";
|
|
6
|
+
import { analyzeStakingOperation, getTimestampRangeFromBlockHeight } from "./utils";
|
|
5
7
|
|
|
6
8
|
jest.mock("./getBlockInfo");
|
|
7
9
|
jest.mock("../network/api");
|
|
@@ -23,6 +25,7 @@ describe("getBlock", () => {
|
|
|
23
25
|
jest.clearAllMocks();
|
|
24
26
|
(getBlockInfo as jest.Mock).mockResolvedValue(mockBlockInfo);
|
|
25
27
|
(getTimestampRangeFromBlockHeight as jest.Mock).mockReturnValue(mockTimestampRange);
|
|
28
|
+
(analyzeStakingOperation as jest.Mock).mockResolvedValue(null);
|
|
26
29
|
});
|
|
27
30
|
|
|
28
31
|
it("should return empty block when no transactions exist", async () => {
|
|
@@ -57,6 +60,7 @@ describe("getBlock", () => {
|
|
|
57
60
|
name: "CRYPTOTRANSFER",
|
|
58
61
|
result: "SUCCESS",
|
|
59
62
|
charged_tx_fee: 100000,
|
|
63
|
+
staking_reward_transfers: [],
|
|
60
64
|
transfers: [],
|
|
61
65
|
token_transfers: [],
|
|
62
66
|
};
|
|
@@ -75,6 +79,7 @@ describe("getBlock", () => {
|
|
|
75
79
|
name: "CRYPTOTRANSFER",
|
|
76
80
|
result: "SUCCESS",
|
|
77
81
|
charged_tx_fee: 67179,
|
|
82
|
+
staking_reward_transfers: [],
|
|
78
83
|
transfers: [
|
|
79
84
|
{
|
|
80
85
|
account: "0.0.999",
|
|
@@ -98,4 +103,281 @@ describe("getBlock", () => {
|
|
|
98
103
|
amount: BigInt(-567179 + 67179),
|
|
99
104
|
});
|
|
100
105
|
});
|
|
106
|
+
|
|
107
|
+
it("should handle token transfers", async () => {
|
|
108
|
+
const mockTx = {
|
|
109
|
+
transaction_id: "0.0.999-1234567890-000000000",
|
|
110
|
+
transaction_hash: "hash",
|
|
111
|
+
name: "CRYPTOTRANSFER",
|
|
112
|
+
result: "SUCCESS",
|
|
113
|
+
charged_tx_fee: 100000,
|
|
114
|
+
staking_reward_transfers: [],
|
|
115
|
+
transfers: [],
|
|
116
|
+
token_transfers: [
|
|
117
|
+
{
|
|
118
|
+
token_id: "0.0.12345",
|
|
119
|
+
account: "0.0.999",
|
|
120
|
+
amount: -1000,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
token_id: "0.0.12345",
|
|
124
|
+
account: "0.0.1001",
|
|
125
|
+
amount: 1000,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
(apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([mockTx]);
|
|
131
|
+
|
|
132
|
+
const result = await getBlock(100);
|
|
133
|
+
|
|
134
|
+
expect(result.transactions[0].operations).toEqual([
|
|
135
|
+
{
|
|
136
|
+
type: "transfer",
|
|
137
|
+
address: "0.0.999",
|
|
138
|
+
asset: {
|
|
139
|
+
type: "hts",
|
|
140
|
+
assetReference: "0.0.12345",
|
|
141
|
+
},
|
|
142
|
+
amount: BigInt(-1000),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: "transfer",
|
|
146
|
+
address: "0.0.1001",
|
|
147
|
+
asset: {
|
|
148
|
+
type: "hts",
|
|
149
|
+
assetReference: "0.0.12345",
|
|
150
|
+
},
|
|
151
|
+
amount: BigInt(1000),
|
|
152
|
+
},
|
|
153
|
+
]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should mark failed transactions", async () => {
|
|
157
|
+
const mockTx = {
|
|
158
|
+
transaction_id: "0.0.999-1234567890-000000000",
|
|
159
|
+
transaction_hash: "hash",
|
|
160
|
+
name: "CRYPTOTRANSFER",
|
|
161
|
+
result: "INSUFFICIENT_ACCOUNT_BALANCE",
|
|
162
|
+
charged_tx_fee: 100000,
|
|
163
|
+
staking_reward_transfers: [],
|
|
164
|
+
transfers: [],
|
|
165
|
+
token_transfers: [],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
(apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([mockTx]);
|
|
169
|
+
|
|
170
|
+
const result = await getBlock(100);
|
|
171
|
+
|
|
172
|
+
expect(result.transactions[0].failed).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should analyze CRYPTOUPDATEACCOUNT transactions for staking", async () => {
|
|
176
|
+
const mockTx = {
|
|
177
|
+
transaction_id: "0.0.999-1234567890-000000000",
|
|
178
|
+
transaction_hash: "hash_update",
|
|
179
|
+
name: HEDERA_TRANSACTION_NAMES.UpdateAccount,
|
|
180
|
+
result: "SUCCESS",
|
|
181
|
+
charged_tx_fee: 22000,
|
|
182
|
+
consensus_timestamp: "1704067210.123456789",
|
|
183
|
+
staking_reward_transfers: [],
|
|
184
|
+
transfers: [],
|
|
185
|
+
token_transfers: [],
|
|
186
|
+
};
|
|
187
|
+
const mockStakingAnalysis: StakingAnalysis = {
|
|
188
|
+
operationType: "DELEGATE",
|
|
189
|
+
targetStakingNodeId: 5,
|
|
190
|
+
previousStakingNodeId: null,
|
|
191
|
+
stakedAmount: BigInt(100),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
(apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([mockTx]);
|
|
195
|
+
(analyzeStakingOperation as jest.Mock).mockResolvedValue(mockStakingAnalysis);
|
|
196
|
+
|
|
197
|
+
const result = await getBlock(100);
|
|
198
|
+
|
|
199
|
+
expect(analyzeStakingOperation).toHaveBeenCalledTimes(1);
|
|
200
|
+
expect(analyzeStakingOperation).toHaveBeenCalledWith("0.0.999", mockTx);
|
|
201
|
+
expect(result.transactions[0].operations).toHaveLength(1);
|
|
202
|
+
expect(result.transactions[0].operations[0]).toEqual({
|
|
203
|
+
type: "other",
|
|
204
|
+
operationType: mockStakingAnalysis.operationType,
|
|
205
|
+
stakedNodeId: mockStakingAnalysis.targetStakingNodeId,
|
|
206
|
+
previousStakedNodeId: mockStakingAnalysis.previousStakingNodeId,
|
|
207
|
+
stakedAmount: mockStakingAnalysis.stakedAmount,
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should handle UNDELEGATE staking operation", async () => {
|
|
212
|
+
const mockTx = {
|
|
213
|
+
transaction_id: "0.0.999-1234567890-000000000",
|
|
214
|
+
transaction_hash: "hash_undelegate",
|
|
215
|
+
name: HEDERA_TRANSACTION_NAMES.UpdateAccount,
|
|
216
|
+
result: "SUCCESS",
|
|
217
|
+
charged_tx_fee: 22000,
|
|
218
|
+
consensus_timestamp: "1704067210.123456789",
|
|
219
|
+
staking_reward_transfers: [],
|
|
220
|
+
transfers: [],
|
|
221
|
+
token_transfers: [],
|
|
222
|
+
};
|
|
223
|
+
const mockStakingAnalysis: StakingAnalysis = {
|
|
224
|
+
operationType: "UNDELEGATE",
|
|
225
|
+
targetStakingNodeId: null,
|
|
226
|
+
previousStakingNodeId: 3,
|
|
227
|
+
stakedAmount: BigInt(100),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
(apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([mockTx]);
|
|
231
|
+
(analyzeStakingOperation as jest.Mock).mockResolvedValue(mockStakingAnalysis);
|
|
232
|
+
|
|
233
|
+
const result = await getBlock(100);
|
|
234
|
+
|
|
235
|
+
expect(result.transactions[0].operations[0]).toEqual({
|
|
236
|
+
type: "other",
|
|
237
|
+
operationType: mockStakingAnalysis.operationType,
|
|
238
|
+
stakedNodeId: mockStakingAnalysis.targetStakingNodeId,
|
|
239
|
+
previousStakedNodeId: mockStakingAnalysis.previousStakingNodeId,
|
|
240
|
+
stakedAmount: mockStakingAnalysis.stakedAmount,
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("should handle REDELEGATE staking operation", async () => {
|
|
245
|
+
const mockTx = {
|
|
246
|
+
transaction_id: "0.0.999-1234567890-000000000",
|
|
247
|
+
transaction_hash: "hash_redelegate",
|
|
248
|
+
name: HEDERA_TRANSACTION_NAMES.UpdateAccount,
|
|
249
|
+
result: "SUCCESS",
|
|
250
|
+
charged_tx_fee: 22000,
|
|
251
|
+
consensus_timestamp: "1704067210.123456789",
|
|
252
|
+
staking_reward_transfers: [],
|
|
253
|
+
transfers: [],
|
|
254
|
+
token_transfers: [],
|
|
255
|
+
};
|
|
256
|
+
const mockStakingAnalysis: StakingAnalysis = {
|
|
257
|
+
operationType: "REDELEGATE",
|
|
258
|
+
targetStakingNodeId: 10,
|
|
259
|
+
previousStakingNodeId: 5,
|
|
260
|
+
stakedAmount: BigInt(100),
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
(apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([mockTx]);
|
|
264
|
+
(analyzeStakingOperation as jest.Mock).mockResolvedValue(mockStakingAnalysis);
|
|
265
|
+
|
|
266
|
+
const result = await getBlock(100);
|
|
267
|
+
|
|
268
|
+
expect(result.transactions[0].operations).toEqual([
|
|
269
|
+
{
|
|
270
|
+
type: "other",
|
|
271
|
+
operationType: mockStakingAnalysis.operationType,
|
|
272
|
+
stakedNodeId: mockStakingAnalysis.targetStakingNodeId,
|
|
273
|
+
previousStakedNodeId: mockStakingAnalysis.previousStakingNodeId,
|
|
274
|
+
stakedAmount: mockStakingAnalysis.stakedAmount,
|
|
275
|
+
},
|
|
276
|
+
]);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("should create CLAIM_REWARDS operations for staking reward transfers", async () => {
|
|
280
|
+
const mockTx = {
|
|
281
|
+
transaction_id: "0.0.999-1234567890-000000000",
|
|
282
|
+
transaction_hash: "hash",
|
|
283
|
+
name: "CRYPTOTRANSFER",
|
|
284
|
+
result: "SUCCESS",
|
|
285
|
+
charged_tx_fee: 100000,
|
|
286
|
+
staking_reward_transfers: [
|
|
287
|
+
{
|
|
288
|
+
account: "0.0.999",
|
|
289
|
+
amount: 100000,
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
account: "0.0.1001",
|
|
293
|
+
amount: 200000,
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
transfers: [
|
|
297
|
+
{
|
|
298
|
+
account: "0.0.999",
|
|
299
|
+
amount: -600000,
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
account: "0.0.1001",
|
|
303
|
+
amount: 500000,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
token_transfers: [],
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
(apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([mockTx]);
|
|
310
|
+
|
|
311
|
+
const result = await getBlock(100);
|
|
312
|
+
|
|
313
|
+
expect(result.transactions[0].operations).toEqual([
|
|
314
|
+
{
|
|
315
|
+
type: "transfer",
|
|
316
|
+
address: "0.0.999",
|
|
317
|
+
asset: { type: "native" },
|
|
318
|
+
amount: BigInt(-500000),
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
type: "transfer",
|
|
322
|
+
address: "0.0.1001",
|
|
323
|
+
asset: { type: "native" },
|
|
324
|
+
amount: BigInt(500000),
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
type: "transfer",
|
|
328
|
+
address: "0.0.999",
|
|
329
|
+
asset: { type: "native" },
|
|
330
|
+
amount: BigInt(100000),
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
type: "transfer",
|
|
334
|
+
address: "0.0.1001",
|
|
335
|
+
asset: { type: "native" },
|
|
336
|
+
amount: BigInt(200000),
|
|
337
|
+
},
|
|
338
|
+
]);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("should handle CRYPTOUPDATEACCOUNT if it's not related to staking", async () => {
|
|
342
|
+
const mockTx = {
|
|
343
|
+
transaction_id: "0.0.999-1234567890-000000000",
|
|
344
|
+
transaction_hash: "hash_regular_update",
|
|
345
|
+
name: HEDERA_TRANSACTION_NAMES.UpdateAccount,
|
|
346
|
+
result: "SUCCESS",
|
|
347
|
+
charged_tx_fee: 22000,
|
|
348
|
+
consensus_timestamp: "1704067210.123456789",
|
|
349
|
+
staking_reward_transfers: [],
|
|
350
|
+
transfers: [
|
|
351
|
+
{
|
|
352
|
+
account: "0.0.999",
|
|
353
|
+
amount: -23000,
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
account: "0.0.1000",
|
|
357
|
+
amount: 1000,
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
token_transfers: [],
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
(apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([mockTx]);
|
|
364
|
+
(analyzeStakingOperation as jest.Mock).mockResolvedValue(null);
|
|
365
|
+
|
|
366
|
+
const result = await getBlock(100);
|
|
367
|
+
|
|
368
|
+
expect(result.transactions[0].operations).toEqual([
|
|
369
|
+
{
|
|
370
|
+
type: "transfer",
|
|
371
|
+
address: "0.0.999",
|
|
372
|
+
asset: { type: "native" },
|
|
373
|
+
amount: BigInt(-1000),
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
type: "transfer",
|
|
377
|
+
address: "0.0.1000",
|
|
378
|
+
asset: { type: "native" },
|
|
379
|
+
amount: BigInt(1000),
|
|
380
|
+
},
|
|
381
|
+
]);
|
|
382
|
+
});
|
|
101
383
|
});
|
package/src/logic/getBlock.ts
CHANGED
|
@@ -4,10 +4,19 @@ import type {
|
|
|
4
4
|
BlockOperation,
|
|
5
5
|
BlockTransaction,
|
|
6
6
|
} from "@ledgerhq/coin-framework/api/types";
|
|
7
|
+
import { HEDERA_TRANSACTION_NAMES } from "../constants";
|
|
7
8
|
import { getBlockInfo } from "./getBlockInfo";
|
|
8
9
|
import { apiClient } from "../network/api";
|
|
9
|
-
import type {
|
|
10
|
-
|
|
10
|
+
import type {
|
|
11
|
+
HederaMirrorCoinTransfer,
|
|
12
|
+
HederaMirrorTokenTransfer,
|
|
13
|
+
HederaMirrorTransaction,
|
|
14
|
+
} from "../types";
|
|
15
|
+
import {
|
|
16
|
+
getMemoFromBase64,
|
|
17
|
+
analyzeStakingOperation,
|
|
18
|
+
getTimestampRangeFromBlockHeight,
|
|
19
|
+
} from "./utils";
|
|
11
20
|
|
|
12
21
|
function toHederaAsset(
|
|
13
22
|
mirrorTransfer: HederaMirrorCoinTransfer | HederaMirrorTokenTransfer,
|
|
@@ -45,18 +54,60 @@ function toBlockOperation(
|
|
|
45
54
|
};
|
|
46
55
|
}
|
|
47
56
|
|
|
57
|
+
function createStakingRewardOperations(tx: HederaMirrorTransaction): BlockOperation[] {
|
|
58
|
+
return tx.staking_reward_transfers.map(rewardTransfer => ({
|
|
59
|
+
type: "transfer",
|
|
60
|
+
address: rewardTransfer.account,
|
|
61
|
+
asset: { type: "native" },
|
|
62
|
+
amount: BigInt(rewardTransfer.amount),
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
export async function getBlock(height: number): Promise<Block> {
|
|
49
67
|
const { start, end } = getTimestampRangeFromBlockHeight(height);
|
|
50
68
|
const blockInfo = await getBlockInfo(height);
|
|
51
69
|
const transactions = await apiClient.getTransactionsByTimestampRange(start, end);
|
|
52
70
|
|
|
71
|
+
// analyze CRYPTOUPDATEACCOUNT transactions to distinguish staking operations from regular account updates.
|
|
72
|
+
// this creates a map of transaction_hash -> StakingAnalysis to avoid repeated lookups.
|
|
73
|
+
const stakingAnalyses = await Promise.all(
|
|
74
|
+
transactions
|
|
75
|
+
.filter(tx => tx.name === HEDERA_TRANSACTION_NAMES.UpdateAccount)
|
|
76
|
+
.map(async tx => {
|
|
77
|
+
const payerAccount = tx.transaction_id.split("-")[0];
|
|
78
|
+
const analysis = await analyzeStakingOperation(payerAccount, tx);
|
|
79
|
+
|
|
80
|
+
return [tx.transaction_hash, analysis] as const;
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
const stakingAnalysisMap = new Map(stakingAnalyses);
|
|
84
|
+
|
|
53
85
|
const blockTransactions: BlockTransaction[] = transactions.map(tx => {
|
|
54
86
|
const payerAccount = tx.transaction_id.split("-")[0];
|
|
55
|
-
const
|
|
87
|
+
const stakingAnalysis = stakingAnalysisMap.get(tx.transaction_hash);
|
|
88
|
+
|
|
89
|
+
let operations: BlockOperation[];
|
|
90
|
+
|
|
91
|
+
if (stakingAnalysis) {
|
|
92
|
+
operations = [
|
|
93
|
+
{
|
|
94
|
+
type: "other",
|
|
95
|
+
operationType: stakingAnalysis.operationType,
|
|
96
|
+
stakedNodeId: stakingAnalysis.targetStakingNodeId,
|
|
97
|
+
previousStakedNodeId: stakingAnalysis.previousStakingNodeId,
|
|
98
|
+
stakedAmount: stakingAnalysis.stakedAmount,
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
} else {
|
|
102
|
+
const allTransfers = [...tx.transfers, ...tx.token_transfers];
|
|
103
|
+
operations = allTransfers.map(transfer =>
|
|
104
|
+
toBlockOperation(payerAccount, tx.charged_tx_fee, transfer),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
56
107
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
);
|
|
108
|
+
// add staking reward operations if present (can occur on any transaction type)
|
|
109
|
+
const rewardOperations = createStakingRewardOperations(tx);
|
|
110
|
+
operations.push(...rewardOperations);
|
|
60
111
|
|
|
61
112
|
return {
|
|
62
113
|
hash: tx.transaction_hash,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getValidators } from "./getValidators";
|
|
2
|
+
import { apiClient } from "../network/api";
|
|
3
|
+
|
|
4
|
+
jest.mock("../network/api");
|
|
5
|
+
|
|
6
|
+
describe("getValidators", () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
jest.clearAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should return formatted validators with APY", async () => {
|
|
12
|
+
(apiClient.getNodes as jest.Mock).mockResolvedValue({
|
|
13
|
+
nodes: [
|
|
14
|
+
{
|
|
15
|
+
node_id: 1,
|
|
16
|
+
node_account_id: "0.0.3",
|
|
17
|
+
description: "Hosted by Ledger | Paris, France",
|
|
18
|
+
stake: 1000000,
|
|
19
|
+
reward_rate_start: 3538,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
nextCursor: null,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const result = await getValidators();
|
|
26
|
+
|
|
27
|
+
expect(result.items).toHaveLength(1);
|
|
28
|
+
expect(result.items[0]).toMatchObject({
|
|
29
|
+
address: "0.0.3",
|
|
30
|
+
nodeId: "1",
|
|
31
|
+
name: "Ledger",
|
|
32
|
+
description: "Hosted by Ledger | Paris, France",
|
|
33
|
+
balance: BigInt(1000000),
|
|
34
|
+
apy: expect.any(Number),
|
|
35
|
+
});
|
|
36
|
+
expect(result.items[0].apy).toBeCloseTo(0.01291, 5);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should handle pagination cursor", async () => {
|
|
40
|
+
(apiClient.getNodes as jest.Mock).mockResolvedValue({
|
|
41
|
+
nodes: [],
|
|
42
|
+
nextCursor: "123",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const result = await getValidators("100");
|
|
46
|
+
|
|
47
|
+
expect(apiClient.getNodes).toHaveBeenCalledWith({ cursor: "100", fetchAllPages: false });
|
|
48
|
+
expect(result.next).toBe("123");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Cursor, Page, Validator } from "@ledgerhq/coin-framework/api/types";
|
|
2
|
+
import { apiClient } from "../network/api";
|
|
3
|
+
import { calculateAPY, extractCompanyFromNodeDescription } from "./utils";
|
|
4
|
+
|
|
5
|
+
export async function getValidators(cursor?: Cursor): Promise<Page<Validator>> {
|
|
6
|
+
const res = await apiClient.getNodes({
|
|
7
|
+
fetchAllPages: false,
|
|
8
|
+
...(cursor && { cursor }),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
next: res.nextCursor ?? undefined,
|
|
13
|
+
items: res.nodes.map(node => ({
|
|
14
|
+
address: node.node_account_id,
|
|
15
|
+
nodeId: node.node_id.toString(),
|
|
16
|
+
name: extractCompanyFromNodeDescription(node.description),
|
|
17
|
+
description: node.description,
|
|
18
|
+
balance: BigInt(node.stake),
|
|
19
|
+
apy: calculateAPY(node.reward_rate_start),
|
|
20
|
+
})),
|
|
21
|
+
};
|
|
22
|
+
}
|
package/src/logic/index.ts
CHANGED