Skip to content

Commit 40a3ce0

Browse files
committed
feat!: partial BaseWallet tx history
BaseWallet will load only last n transactions on initial load BREAKING CHANGE: remove BaseWallet stake pool and drep provider dependency - add RewardAccountInfoProvider as a new BaseWallet dependency
1 parent b52e51f commit 40a3ce0

File tree

57 files changed

+1277
-2702
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1277
-2702
lines changed

.github/workflows/continuous-integration-blockfrost-e2e.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ env:
2626
TEST_CLIENT_TX_SUBMIT_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4000/"}'
2727
TEST_CLIENT_UTXO_PROVIDER: 'blockfrost'
2828
TEST_CLIENT_UTXO_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:3015"}'
29+
TEST_CLIENT_REWARD_ACCOUNT_INFO_PROVIDER: 'blockfrost'
30+
TEST_CLIENT_REWARD_ACCOUNT_INFO_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:3015"}'
2931
TEST_CLIENT_STAKE_POOL_PROVIDER: 'http'
3032
TEST_CLIENT_STAKE_POOL_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4000/"}'
3133
WS_PROVIDER_URL: 'http://localhost:4100/ws'

.github/workflows/continuous-integration-e2e.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ env:
2828
TEST_CLIENT_UTXO_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4000/"}'
2929
TEST_CLIENT_STAKE_POOL_PROVIDER: 'http'
3030
TEST_CLIENT_STAKE_POOL_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4000/"}'
31+
TEST_CLIENT_REWARD_ACCOUNT_INFO_PROVIDER: 'blockfrost'
32+
TEST_CLIENT_REWARD_ACCOUNT_INFO_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:3015"}'
3133
WS_PROVIDER_URL: 'http://localhost:4100/ws'
3234

3335
on:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { Cardano, DRepProvider, RewardAccountInfoProvider, Serialization, StakePoolProvider } from '@cardano-sdk/core';
2+
3+
import { BlockfrostClient, BlockfrostProvider, fetchSequentially, isBlockfrostNotFoundError } from '../blockfrost';
4+
import { HexBlob, isNotNil } from '@cardano-sdk/util';
5+
import { Logger } from 'ts-log';
6+
import uniq from 'lodash/uniq.js';
7+
import type { Responses } from '@blockfrost/blockfrost-js';
8+
9+
export type BlockfrostRewardAccountInfoProviderDependencies = {
10+
client: BlockfrostClient;
11+
logger: Logger;
12+
stakePoolProvider: StakePoolProvider;
13+
dRepProvider: DRepProvider;
14+
};
15+
16+
const emptyArrayIfNotFound = (error: unknown) => {
17+
if (isBlockfrostNotFoundError(error)) {
18+
return [];
19+
}
20+
throw error;
21+
};
22+
23+
export class BlockfrostRewardAccountInfoProvider extends BlockfrostProvider implements RewardAccountInfoProvider {
24+
#dRepProvider: DRepProvider;
25+
#stakePoolProvider: StakePoolProvider;
26+
27+
constructor({ client, logger, stakePoolProvider, dRepProvider }: BlockfrostRewardAccountInfoProviderDependencies) {
28+
super(client, logger);
29+
this.#dRepProvider = dRepProvider;
30+
this.#stakePoolProvider = stakePoolProvider;
31+
}
32+
33+
async rewardAccountInfo(
34+
address: Cardano.RewardAccount,
35+
localEpoch: Cardano.EpochNo
36+
): Promise<Cardano.RewardAccountInfo> {
37+
const [account, [lastRegistrationActivity]] = await Promise.all([
38+
await this.request<Responses['account_content']>(`accounts/${address}`).catch(
39+
(error): Responses['account_content'] => {
40+
if (isBlockfrostNotFoundError(error)) {
41+
return {
42+
active: false,
43+
active_epoch: null,
44+
controlled_amount: '0',
45+
drep_id: null,
46+
pool_id: null,
47+
reserves_sum: '0',
48+
rewards_sum: '0',
49+
stake_address: address,
50+
treasury_sum: '0',
51+
withdrawable_amount: '0',
52+
withdrawals_sum: '0'
53+
};
54+
}
55+
throw error;
56+
}
57+
),
58+
this.request<Responses['account_registration_content']>(
59+
`accounts/${address}/registrations?order=desc&count=1`
60+
).catch(emptyArrayIfNotFound)
61+
]);
62+
63+
const isUnregisteringAtEpoch = await this.#getUnregisteringAtEpoch(lastRegistrationActivity);
64+
65+
const credentialStatus = account.active
66+
? Cardano.StakeCredentialStatus.Registered
67+
: lastRegistrationActivity?.action === 'registered'
68+
? Cardano.StakeCredentialStatus.Registering
69+
: typeof isUnregisteringAtEpoch === 'undefined' || isUnregisteringAtEpoch <= localEpoch
70+
? Cardano.StakeCredentialStatus.Unregistered
71+
: Cardano.StakeCredentialStatus.Unregistering;
72+
const rewardBalance = BigInt(account.withdrawable_amount || '0');
73+
74+
const [delegatee, dRepDelegatee, deposit] = await Promise.all([
75+
this.#getDelegatee(address, localEpoch, isUnregisteringAtEpoch),
76+
this.#getDrepDelegatee(account),
77+
// This provider currently does not find other deposits (pool/drep/govaction)
78+
this.#getKeyDeposit(lastRegistrationActivity)
79+
]);
80+
81+
return {
82+
address,
83+
credentialStatus,
84+
dRepDelegatee,
85+
delegatee,
86+
deposit,
87+
rewardBalance
88+
};
89+
}
90+
91+
async delegationPortfolio(rewardAccount: Cardano.RewardAccount): Promise<Cardano.Cip17DelegationPortfolio | null> {
92+
const portfolios = await fetchSequentially({
93+
haveEnoughItems: (items: Array<null | Cardano.Cip17DelegationPortfolio>) => items.some(isNotNil),
94+
paginationOptions: { order: 'desc' },
95+
request: async (paginationQueryString) => {
96+
const txs = await this.request<Responses['account_delegation_content']>(
97+
`accounts/${rewardAccount}/delegations?${paginationQueryString}`
98+
).catch(emptyArrayIfNotFound);
99+
const result: Array<null | Cardano.Cip17DelegationPortfolio> = [];
100+
for (const { tx_hash } of txs) {
101+
const metadata = await this.request<Responses['tx_content_metadata_cbor']>(
102+
`txs/${tx_hash}/metadata/cbor`
103+
).catch(emptyArrayIfNotFound);
104+
const cbor = metadata.find(({ label }) => label === '6862')?.metadata;
105+
if (!cbor) {
106+
result.push(null);
107+
continue;
108+
}
109+
const metadatum = Serialization.TransactionMetadatum.fromCbor(HexBlob(cbor));
110+
try {
111+
result.push(Cardano.cip17FromMetadatum(metadatum.toCore()));
112+
break;
113+
} catch {
114+
result.push(null);
115+
}
116+
}
117+
return result;
118+
}
119+
});
120+
return portfolios.find(isNotNil) || null;
121+
}
122+
123+
async #getUnregisteringAtEpoch(
124+
lastRegistrationActivity: Responses['account_registration_content'][0] | undefined
125+
): Promise<Cardano.EpochNo | undefined> {
126+
if (!lastRegistrationActivity || lastRegistrationActivity.action === 'registered') {
127+
return;
128+
}
129+
const tx = await this.request<Responses['tx_content']>(`txs/${lastRegistrationActivity.tx_hash}`);
130+
const block = await this.request<Responses['block_content']>(`blocks/${tx.block}`);
131+
return Cardano.EpochNo(block.epoch!);
132+
}
133+
134+
async #getDrepDelegatee(account: Responses['account_content']): Promise<Cardano.DRepDelegatee | undefined> {
135+
if (!account.drep_id) return;
136+
if (account.drep_id === 'drep_always_abstain') {
137+
return { delegateRepresentative: { __typename: 'AlwaysAbstain' } };
138+
}
139+
if (account.drep_id === 'drep_always_no_confidence') {
140+
return { delegateRepresentative: { __typename: 'AlwaysNoConfidence' } };
141+
}
142+
const cip129DrepId = Cardano.DRepID.toCip129DRepID(Cardano.DRepID(account.drep_id));
143+
const dRepInfo = await this.#dRepProvider.getDRepInfo({ id: cip129DrepId });
144+
return {
145+
delegateRepresentative: dRepInfo
146+
};
147+
}
148+
149+
async #getKeyDeposit(lastRegistrationActivity: Responses['account_registration_content'][0] | undefined) {
150+
if (!lastRegistrationActivity || lastRegistrationActivity.action === 'deregistered') {
151+
return 0n;
152+
}
153+
const tx = await this.request<Responses['tx_content']>(`txs/${lastRegistrationActivity.tx_hash}`);
154+
const block = await this.request<Responses['block_content']>(`blocks/${tx.block}`);
155+
const epochParameters = await this.request<Responses['epoch_param_content']>(`epochs/${block.epoch}/parameters`);
156+
return BigInt(epochParameters.key_deposit);
157+
}
158+
159+
async #getDelegatee(
160+
address: Cardano.RewardAccount,
161+
currentEpoch: Cardano.EpochNo,
162+
isUnregisteringAtEpoch: Cardano.EpochNo | undefined
163+
): Promise<Cardano.Delegatee | undefined> {
164+
const delegationHistory = await fetchSequentially<Responses['account_delegation_content'][0]>({
165+
haveEnoughItems: (items) => items[items.length - 1]?.active_epoch <= currentEpoch,
166+
paginationOptions: { order: 'desc' },
167+
request: (paginationQueryString) => this.request(`accounts/${address}/delegations?${paginationQueryString}`)
168+
});
169+
170+
const isRegisteredAt = (epochFromNow: number): true | undefined => {
171+
if (!isUnregisteringAtEpoch) {
172+
return true;
173+
}
174+
return isUnregisteringAtEpoch > currentEpoch + epochFromNow || undefined;
175+
};
176+
177+
const poolIds = [
178+
// current epoch
179+
isRegisteredAt(0) && delegationHistory.find(({ active_epoch }) => active_epoch <= currentEpoch)?.pool_id,
180+
// next epoch
181+
isRegisteredAt(1) && delegationHistory.find(({ active_epoch }) => active_epoch <= currentEpoch + 1)?.pool_id,
182+
// next next epoch
183+
isRegisteredAt(2) && delegationHistory.find(({ active_epoch }) => active_epoch <= currentEpoch + 2)?.pool_id
184+
] as Array<Cardano.PoolId | undefined>;
185+
186+
const poolIdsToFetch = uniq(poolIds.filter(isNotNil));
187+
if (poolIdsToFetch.length === 0) {
188+
return undefined;
189+
}
190+
191+
const stakePools = await this.#stakePoolProvider.queryStakePools({
192+
filters: { identifier: { values: poolIdsToFetch.map((id) => ({ id })) } },
193+
pagination: { limit: 3, startAt: 0 }
194+
});
195+
196+
const stakePoolMathingPoolId = (index: number) => stakePools.pageResults.find((pool) => pool.id === poolIds[index]);
197+
return {
198+
currentEpoch: stakePoolMathingPoolId(0),
199+
nextEpoch: stakePoolMathingPoolId(1),
200+
nextNextEpoch: stakePoolMathingPoolId(2)
201+
};
202+
}
203+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './RewardAccountInfoProvider';

packages/cardano-services-client/src/RewardsProvider/BlockfrostRewardsProvider.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class BlockfrostRewardsProvider extends BlockfrostProvider implements Rew
3131
}: Range<Cardano.EpochNo> = {}
3232
): Promise<Reward[]> {
3333
const batchSize = 100;
34-
return fetchSequentially<Reward, Responses['account_reward_content'][0]>({
34+
return fetchSequentially<Responses['account_reward_content'][0], Reward>({
3535
haveEnoughItems: (_, rewardsPage) => {
3636
const lastReward = rewardsPage[rewardsPage.length - 1];
3737
return !lastReward || lastReward.epoch >= upperBound;

packages/cardano-services-client/src/blockfrost/util.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const buildQueryString = ({ page, count, order }: PaginationOptions) => {
2222
};
2323

2424
// copied from @cardano-sdk/cardano-services and updated to use custom blockfrost client instead of blockfrost-js
25-
export const fetchSequentially = async <Item, Response>(
25+
export const fetchSequentially = async <Response, Item = Response>(
2626
props: {
2727
request: (paginationQueryString: string) => Promise<Response[]>;
2828
responseTranslator?: (response: Response[]) => Item[];
@@ -42,7 +42,7 @@ export const fetchSequentially = async <Item, Response>(
4242
const newAccumulatedItems = [...accumulated, ...maybeTranslatedResponse] as Item[];
4343
const haveEnoughItems = props.haveEnoughItems?.(newAccumulatedItems, response);
4444
if (response.length === count && !haveEnoughItems) {
45-
return fetchSequentially<Item, Response>(props, page + 1, newAccumulatedItems);
45+
return fetchSequentially<Response, Item>(props, page + 1, newAccumulatedItems);
4646
}
4747
return newAccumulatedItems;
4848
} catch (error) {

packages/cardano-services-client/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './StakePoolProvider';
66
export * from './UtxoProvider';
77
export * from './ChainHistoryProvider';
88
export * from './DRepProvider';
9+
export * from './RewardAccountInfoProvider';
910
export * from './NetworkInfoProvider';
1011
export * from './RewardsProvider';
1112
export * from './HandleProvider';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './types';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Cardano, Provider } from '../..';
2+
3+
export interface RewardAccountInfoProvider extends Provider {
4+
rewardAccountInfo(
5+
rewardAccount: Cardano.RewardAccount,
6+
localEpoch: Cardano.EpochNo
7+
): Promise<Cardano.RewardAccountInfo>;
8+
delegationPortfolio(rewardAccounts: Cardano.RewardAccount): Promise<Cardano.Cip17DelegationPortfolio | null>;
9+
}

packages/core/src/Provider/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './Provider';
22
export * from './StakePoolProvider';
33
export * from './AssetProvider';
44
export * from './NetworkInfoProvider';
5+
export * from './RewardAccountInfoProvider';
56
export * from './RewardsProvider';
67
export * from './TxSubmitProvider';
78
export * as ProviderUtil from './providerUtil';

packages/e2e/.env.example

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ TEST_CLIENT_HANDLE_PROVIDER=http
2020
TEST_CLIENT_HANDLE_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4011/"}'
2121
TEST_CLIENT_NETWORK_INFO_PROVIDER=ws
2222
TEST_CLIENT_NETWORK_INFO_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4000/"}'
23+
TEST_CLIENT_REWARD_ACCOUNT_INFO_PROVIDER=blockfrost
24+
TEST_CLIENT_REWARD_ACCOUNT_INFO_PROVIDER_PARAMS='{"baseUrl":"http://localhost:3015"}'
2325
TEST_CLIENT_REWARDS_PROVIDER=http
2426
TEST_CLIENT_REWARDS_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4000/"}'
2527
TEST_CLIENT_TX_SUBMIT_PROVIDER=http

packages/e2e/src/environment.ts

+4
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ const validators = {
9898
TEST_CLIENT_HANDLE_PROVIDER_PARAMS: providerParams(),
9999
TEST_CLIENT_NETWORK_INFO_PROVIDER: str(),
100100
TEST_CLIENT_NETWORK_INFO_PROVIDER_PARAMS: providerParams(),
101+
TEST_CLIENT_REWARD_ACCOUNT_INFO_PROVIDER: str(),
102+
TEST_CLIENT_REWARD_ACCOUNT_INFO_PROVIDER_PARAMS: providerParams(),
101103
TEST_CLIENT_REWARDS_PROVIDER: str(),
102104
TEST_CLIENT_REWARDS_PROVIDER_PARAMS: providerParams(),
103105
TEST_CLIENT_STAKE_POOL_PROVIDER: str(),
@@ -158,6 +160,8 @@ export const walletVariables = [
158160
'TEST_CLIENT_NETWORK_INFO_PROVIDER_PARAMS',
159161
'TEST_CLIENT_REWARDS_PROVIDER',
160162
'TEST_CLIENT_REWARDS_PROVIDER_PARAMS',
163+
'TEST_CLIENT_REWARD_ACCOUNT_INFO_PROVIDER',
164+
'TEST_CLIENT_REWARD_ACCOUNT_INFO_PROVIDER_PARAMS',
161165
'TEST_CLIENT_STAKE_POOL_PROVIDER',
162166
'TEST_CLIENT_STAKE_POOL_PROVIDER_PARAMS',
163167
'TEST_CLIENT_TX_SUBMIT_PROVIDER',

0 commit comments

Comments
 (0)