Skip to content

Commit e2159e1

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 f5bee9f commit e2159e1

File tree

56 files changed

+970
-2482
lines changed

Some content is hidden

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

56 files changed

+970
-2482
lines changed

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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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:
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { Cardano, DRepProvider, RewardAccountInfoProvider, StakePoolProvider } from '@cardano-sdk/core';
2+
3+
import { BlockfrostClient, BlockfrostProvider, fetchSequentially, isBlockfrostNotFoundError } from '../blockfrost';
4+
import { Logger } from 'ts-log';
5+
import { isNotNil } from '@cardano-sdk/util';
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+
export class BlockfrostRewardAccountInfoProvider extends BlockfrostProvider implements RewardAccountInfoProvider {
17+
#dRepProvider: DRepProvider;
18+
#stakePoolProvider: StakePoolProvider;
19+
20+
constructor({ client, logger, stakePoolProvider, dRepProvider }: BlockfrostRewardAccountInfoProviderDependencies) {
21+
super(client, logger);
22+
this.#dRepProvider = dRepProvider;
23+
this.#stakePoolProvider = stakePoolProvider;
24+
}
25+
26+
async rewardAccountInfo(address: Cardano.RewardAccount): Promise<Cardano.RewardAccountInfo> {
27+
const [account, [lastRegistrationActivity]] = await Promise.all([
28+
await this.request<Responses['account_content']>(`accounts/${address}`).catch(
29+
(error): Responses['account_content'] => {
30+
if (isBlockfrostNotFoundError(error)) {
31+
return {
32+
active: false,
33+
active_epoch: null,
34+
controlled_amount: '0',
35+
drep_id: null,
36+
pool_id: null,
37+
reserves_sum: '0',
38+
rewards_sum: '0',
39+
stake_address: address,
40+
treasury_sum: '0',
41+
withdrawable_amount: '0',
42+
withdrawals_sum: '0'
43+
};
44+
}
45+
throw error;
46+
}
47+
),
48+
this.request<Responses['account_registration_content']>(
49+
`accounts/${address}/registrations?order=desc&count=1`
50+
).catch((error: unknown) => {
51+
if (isBlockfrostNotFoundError(error)) {
52+
return [];
53+
}
54+
throw error;
55+
})
56+
]);
57+
58+
// This provider maps 'Unregistering' into 'Unregistered',
59+
// because it is expensive to discriminate between those statuses (would have to fetch tx, tip, era summaries)
60+
// There is currently no logic within cardano-js-sdk that would work differently because of that.
61+
const credentialStatus = account.active
62+
? Cardano.StakeCredentialStatus.Registered
63+
: lastRegistrationActivity?.action === 'registered'
64+
? Cardano.StakeCredentialStatus.Registering
65+
: Cardano.StakeCredentialStatus.Unregistered;
66+
const rewardBalance = BigInt(account.withdrawable_amount || '0');
67+
68+
const [delegatee, dRepDelegatee, deposit] = await Promise.all([
69+
this.#getDelegatee(address),
70+
this.#getDrepDelegatee(account),
71+
// This provider currently does not find other deposits (pool/drep/govaction)
72+
await this.#getKeyDeposit(lastRegistrationActivity)
73+
]);
74+
75+
return {
76+
address,
77+
credentialStatus,
78+
dRepDelegatee,
79+
delegatee,
80+
deposit,
81+
rewardBalance
82+
};
83+
}
84+
85+
async delegationPortfolio(rewardAccounts: Cardano.RewardAccount[]): Promise<Cardano.Cip17DelegationPortfolio | null> {
86+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
87+
rewardAccounts;
88+
// TODO;
89+
return null;
90+
}
91+
92+
async #getDrepDelegatee(account: Responses['account_content']): Promise<Cardano.DRepDelegatee | undefined> {
93+
if (!account.drep_id) return;
94+
const dRepInfo = await this.#dRepProvider.getDRepInfo({ id: Cardano.DRepID(account.drep_id) });
95+
return {
96+
// TODO: always abstain or always no confidence
97+
delegateRepresentative: dRepInfo
98+
};
99+
}
100+
101+
async #getKeyDeposit(lastRegistrationActivity: Responses['account_registration_content'][0] | undefined) {
102+
if (!lastRegistrationActivity || lastRegistrationActivity.action === 'deregistered') {
103+
return 0n;
104+
}
105+
const tx = await this.request<Responses['tx_content']>(`txs/${lastRegistrationActivity.tx_hash}`);
106+
const block = await this.request<Responses['block_content']>(`blocks/${tx.block}`);
107+
const epochParameters = await this.request<Responses['epoch_param_content']>(`epochs/${block.epoch}/parameters`);
108+
return BigInt(epochParameters.key_deposit);
109+
}
110+
111+
async #getDelegatee(address: Cardano.RewardAccount): Promise<Cardano.Delegatee | undefined> {
112+
const latestEpoch = await this.request<Responses['epoch_content']>('epochs/latest');
113+
const delegationHistory = await fetchSequentially<Responses['account_delegation_content'][0]>({
114+
haveEnoughItems: (items) => items[items.length - 1]?.active_epoch <= latestEpoch.epoch,
115+
paginationOptions: { order: 'desc' },
116+
request: (paginationQueryString) => this.request(`accounts/${address}/delegations?${paginationQueryString}`)
117+
});
118+
119+
const poolIds = [
120+
delegationHistory.find(({ active_epoch }) => active_epoch <= latestEpoch.epoch)?.pool_id,
121+
delegationHistory.find(({ active_epoch }) => active_epoch <= latestEpoch.epoch + 1)?.pool_id,
122+
delegationHistory.find(({ active_epoch }) => active_epoch <= latestEpoch.epoch + 2)?.pool_id
123+
] as Array<Cardano.PoolId | undefined>;
124+
125+
const poolIdsToFetch = uniq(poolIds.filter(isNotNil));
126+
if (poolIdsToFetch.length === 0) {
127+
return undefined;
128+
}
129+
130+
const stakePools = await this.#stakePoolProvider.queryStakePools({
131+
filters: { identifier: { values: poolIdsToFetch.map((id) => ({ id })) } },
132+
pagination: { limit: 3, startAt: 0 }
133+
});
134+
135+
const stakePoolMathingPoolId = (index: number) => stakePools.pageResults.find((pool) => pool.id === poolIds[index]);
136+
return {
137+
currentEpoch: stakePoolMathingPoolId(0),
138+
nextEpoch: stakePoolMathingPoolId(1),
139+
nextNextEpoch: stakePoolMathingPoolId(2)
140+
};
141+
}
142+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './RewardAccountInfoProvider';

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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 1 addition & 0 deletions
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';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './types';
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Cardano, Provider } from '../..';
2+
3+
export interface RewardAccountInfoProvider extends Provider {
4+
rewardAccountInfo(rewardAccount: Cardano.RewardAccount): Promise<Cardano.RewardAccountInfo>;
5+
delegationPortfolio(rewardAccounts: Cardano.RewardAccount[]): Promise<Cardano.Cip17DelegationPortfolio | null>;
6+
}

packages/core/src/Provider/index.ts

Lines changed: 1 addition & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 4 additions & 0 deletions
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',

packages/e2e/src/factories.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
HandleProvider,
2323
NetworkInfoProvider,
2424
ProviderFactory,
25+
RewardAccountInfoProvider,
2526
RewardsProvider,
2627
StakePoolProvider,
2728
TxSubmitProvider,
@@ -42,6 +43,7 @@ import {
4243
BlockfrostClient,
4344
BlockfrostDRepProvider,
4445
BlockfrostNetworkInfoProvider,
46+
BlockfrostRewardAccountInfoProvider,
4547
BlockfrostRewardsProvider,
4648
BlockfrostTxSubmitProvider,
4749
BlockfrostUtxoProvider,
@@ -84,6 +86,7 @@ export const keyManagementFactory = new ProviderFactory<CreateKeyAgent>();
8486
export const assetProviderFactory = new ProviderFactory<AssetProvider>();
8587
export const chainHistoryProviderFactory = new ProviderFactory<ChainHistoryProvider>();
8688
export const drepProviderFactory = new ProviderFactory<DRepProvider>();
89+
export const rewardAccountInfoProviderFactory = new ProviderFactory<RewardAccountInfoProvider>();
8790
export const networkInfoProviderFactory = new ProviderFactory<NetworkInfoProvider>();
8891
export const rewardsProviderFactory = new ProviderFactory<RewardsProvider>();
8992
export const txSubmitProviderFactory = new ProviderFactory<TxSubmitProvider>();
@@ -206,6 +209,36 @@ drepProviderFactory.register(BLOCKFROST_PROVIDER, async (params: any, logger): P
206209
});
207210
});
208211

212+
rewardAccountInfoProviderFactory.register(
213+
BLOCKFROST_PROVIDER,
214+
async (params: any, logger): Promise<RewardAccountInfoProvider> => {
215+
if (params.baseUrl === undefined) throw new Error(`${BlockfrostDRepProvider.name}: ${MISSING_URL_PARAM}`);
216+
const env = getEnv(walletVariables);
217+
218+
return new Promise<RewardAccountInfoProvider>(async (resolve) => {
219+
resolve(
220+
new BlockfrostRewardAccountInfoProvider({
221+
client: new BlockfrostClient(
222+
{ apiVersion: params.apiVersion, baseUrl: params.baseUrl, projectId: params.projectId },
223+
{ rateLimiter: { schedule: (task) => task() } }
224+
),
225+
dRepProvider: await drepProviderFactory.create(
226+
BLOCKFROST_PROVIDER,
227+
env.TEST_CLIENT_DREP_PROVIDER_PARAMS,
228+
logger
229+
),
230+
logger,
231+
stakePoolProvider: await stakePoolProviderFactory.create(
232+
HTTP_PROVIDER,
233+
env.TEST_CLIENT_STAKE_POOL_PROVIDER_PARAMS,
234+
logger
235+
)
236+
})
237+
);
238+
});
239+
}
240+
);
241+
209242
networkInfoProviderFactory.register(
210243
HTTP_PROVIDER,
211244
async (params: any, logger: Logger): Promise<NetworkInfoProvider> => {
@@ -525,11 +558,6 @@ export const getWallet = async (props: GetWalletProps) => {
525558
env.TEST_CLIENT_CHAIN_HISTORY_PROVIDER_PARAMS,
526559
logger
527560
),
528-
drepProvider: await drepProviderFactory.create(
529-
env.TEST_CLIENT_DREP_PROVIDER,
530-
env.TEST_CLIENT_DREP_PROVIDER_PARAMS,
531-
logger
532-
),
533561
handleProvider: await handleProviderFactory.create(
534562
env.TEST_CLIENT_HANDLE_PROVIDER,
535563
env.TEST_CLIENT_HANDLE_PROVIDER_PARAMS,
@@ -540,6 +568,11 @@ export const getWallet = async (props: GetWalletProps) => {
540568
env.TEST_CLIENT_NETWORK_INFO_PROVIDER_PARAMS,
541569
logger
542570
),
571+
rewardAccountInfoProvider: await rewardAccountInfoProviderFactory.create(
572+
env.TEST_CLIENT_REWARD_ACCOUNT_INFO_PROVIDER,
573+
env.TEST_CLIENT_REWARD_ACCOUNT_INFO_PROVIDER_PARAMS,
574+
logger
575+
),
543576
rewardsProvider: await rewardsProviderFactory.create(
544577
env.TEST_CLIENT_REWARDS_PROVIDER,
545578
env.TEST_CLIENT_REWARDS_PROVIDER_PARAMS,
@@ -633,6 +666,11 @@ export const getSharedWallet = async (props: GetSharedWalletProps) => {
633666
env.TEST_CLIENT_NETWORK_INFO_PROVIDER_PARAMS,
634667
logger
635668
),
669+
rewardAccountInfoProvider: await rewardAccountInfoProviderFactory.create(
670+
env.TEST_CLIENT_REWARD_ACCOUNT_INFO_PROVIDER,
671+
env.TEST_CLIENT_REWARD_ACCOUNT_INFO_PARAMS,
672+
logger
673+
),
636674
rewardsProvider: await rewardsProviderFactory.create(
637675
env.TEST_CLIENT_REWARDS_PROVIDER,
638676
env.TEST_CLIENT_REWARDS_PROVIDER_PARAMS,

packages/e2e/src/tools/multi-delegation-data-gen/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ const monitor = new TerminalProgressMonitor();
5656

5757
monitor.logInfo(`Output directory: ${chalk.green!(Files.combine([process.cwd(), outputPath]))}`);
5858

59-
fundingWallet = await waitForFundingWallet(monitor);
59+
const { providers, wallet } = await waitForFundingWallet(monitor);
60+
fundingWallet = wallet;
6061

6162
const delegationWallet = await createDelegationWallet(monitor);
6263

@@ -68,7 +69,12 @@ const monitor = new TerminalProgressMonitor();
6869

6970
monitor.endTask('Delegation wallet ready.', TaskResult.Success);
7071

71-
const portfolio = await distributeStake(delegationWallet, config.stakeDistribution, monitor);
72+
const portfolio = await distributeStake(
73+
delegationWallet,
74+
providers.stakePoolProvider,
75+
config.stakeDistribution,
76+
monitor
77+
);
7278

7379
monitor.startTask('Waiting for delegation to be updated on the wallet.');
7480
await rewardAccountStatuses(delegationWallet.delegation.rewardAccounts$, [

0 commit comments

Comments
 (0)