Skip to content

Commit 7aa1397

Browse files
author
Aaron Cook
authored
feat: map vault redeem transactions (#2551)
Adds mapping of vault-specific `redeem` transactions, refactoring mapping of `deposit` accordingly. The staking datasource has been extended to retrieve DeFi stakes, and Morpho extra rewards, required for determining the balance of staking (additional) rewards. These are included/validated in their respective repository: - Add new cache keys - Extend `KilnApi` with new endpoints for DeFi stakes and Morpho extra rewards: - Add/infer tyes from validation schema for the above - Add respective methods to the `StakingRepository`, validating against said schemas - Refactor `mapDepositInfo` and related entities - Add `mapWithdrawInfo`, called in `MultisigTransactionInfoMapper` - Add/update tests accordingly
1 parent 7b36d0e commit 7aa1397

26 files changed

+1384
-177
lines changed

src/datasources/cache/cache.router.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ export class CacheRouter {
4242
'staking_dedicated_staking_stats';
4343
private static readonly STAKING_DEFI_VAULT_STATS_KEY =
4444
'staking_defi_vault_stats';
45+
private static readonly STAKING_DEFI_VAULT_STAKES_KEY =
46+
'staking_defi_vault_stakes';
47+
private static readonly STAKING_DEFI_MORPHO_EXTRA_REWARDS_KEY =
48+
'staking_defi_morpho_extra_rewards';
4549
private static readonly STAKING_DEPLOYMENTS_KEY = 'staking_deployments';
4650
private static readonly STAKING_NETWORK_STATS_KEY = 'staking_network_stats';
4751
private static readonly STAKING_POOLED_STAKING_STATS_KEY =
@@ -624,6 +628,27 @@ export class CacheRouter {
624628
);
625629
}
626630

631+
static getStakingDefiVaultStakesCacheDir(args: {
632+
chainId: string;
633+
safeAddress: `0x${string}`;
634+
vault: `0x${string}`;
635+
}): CacheDir {
636+
return new CacheDir(
637+
`${args.chainId}_${this.STAKING_DEFI_VAULT_STAKES_KEY}_${args.safeAddress}_${args.vault}`,
638+
'',
639+
);
640+
}
641+
642+
static getStakingDefiMorphoExtraRewardsCacheDir(args: {
643+
chainId: string;
644+
safeAddress: `0x${string}`;
645+
}): CacheDir {
646+
return new CacheDir(
647+
`${args.chainId}_${this.STAKING_DEFI_MORPHO_EXTRA_REWARDS_KEY}_${args.safeAddress}`,
648+
'',
649+
);
650+
}
651+
627652
/**
628653
* Calculated the chain/Safe-specific cache key of {@link Stake}.
629654
*
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { faker } from '@faker-js/faker';
2+
import { getAddress } from 'viem';
3+
4+
import { Builder } from '@/__tests__/builder';
5+
import type { IBuilder } from '@/__tests__/builder';
6+
import type { DefiMorphoExtraReward } from '@/datasources/staking-api/entities/defi-morpho-extra-reward.entity';
7+
8+
export function defiMorphoExtraRewardBuilder(): IBuilder<DefiMorphoExtraReward> {
9+
return new Builder<DefiMorphoExtraReward>()
10+
.with('chain_id', faker.number.int())
11+
.with('asset', getAddress(faker.finance.ethereumAddress()))
12+
.with('claimable', faker.string.numeric())
13+
.with('claimable_next', faker.string.numeric());
14+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { faker } from '@faker-js/faker';
2+
import { getAddress } from 'viem';
3+
4+
import { defiMorphoExtraRewardBuilder } from '@/datasources/staking-api/entities/__tests__/defi-morpho-extra-reward.entity.builder';
5+
import { DefiMorphoExtraRewardSchema } from '@/datasources/staking-api/entities/defi-morpho-extra-reward.entity';
6+
7+
describe('DefiMorphoExtraRewardSchema', () => {
8+
it('should validate a DefiMorphoExtraRewardSchema', () => {
9+
const defiMorphoExtraReward = defiMorphoExtraRewardBuilder().build();
10+
11+
const result = DefiMorphoExtraRewardSchema.safeParse(defiMorphoExtraReward);
12+
13+
expect(result.success).toBe(true);
14+
});
15+
16+
it('should checksum the asset address', () => {
17+
const lowerCaseAddress = faker.finance.ethereumAddress().toLowerCase();
18+
const defiMorphoExtraReward = defiMorphoExtraRewardBuilder()
19+
.with('asset', lowerCaseAddress as `0x${string}`)
20+
.build();
21+
22+
const result = DefiMorphoExtraRewardSchema.safeParse(defiMorphoExtraReward);
23+
24+
expect(result.success && result.data.asset).toBe(
25+
getAddress(lowerCaseAddress),
26+
);
27+
});
28+
29+
it.each(['claimable' as const, 'claimable_next' as const])(
30+
'should not allow a non-numerical string for %s',
31+
(field) => {
32+
const defiMorphoExtraReward = defiMorphoExtraRewardBuilder()
33+
.with(field, 'not-a-number')
34+
.build();
35+
36+
const result = DefiMorphoExtraRewardSchema.safeParse(
37+
defiMorphoExtraReward,
38+
);
39+
40+
expect(!result.success && result.error.issues[0]).toStrictEqual({
41+
code: 'custom',
42+
message: 'Invalid base-10 numeric string',
43+
path: [field],
44+
});
45+
},
46+
);
47+
48+
it('should not validate a non-DefiMorphoExtraRewardSchema', () => {
49+
const defiMorphoExtraReward = {
50+
invalid: 'defiMorphoExtraReward',
51+
};
52+
53+
const result = DefiMorphoExtraRewardSchema.safeParse(defiMorphoExtraReward);
54+
55+
expect(!result.success && result.error.issues).toStrictEqual([
56+
{
57+
code: 'invalid_type',
58+
expected: 'number',
59+
message: 'Required',
60+
path: ['chain_id'],
61+
received: 'undefined',
62+
},
63+
{
64+
code: 'invalid_type',
65+
expected: 'string',
66+
message: 'Required',
67+
path: ['asset'],
68+
received: 'undefined',
69+
},
70+
{
71+
code: 'invalid_type',
72+
expected: 'string',
73+
message: 'Required',
74+
path: ['claimable'],
75+
received: 'undefined',
76+
},
77+
{
78+
code: 'invalid_type',
79+
expected: 'string',
80+
message: 'Required',
81+
path: ['claimable_next'],
82+
received: 'undefined',
83+
},
84+
]);
85+
});
86+
});
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { faker } from '@faker-js/faker';
2+
import { getAddress } from 'viem';
3+
import { defiVaultStakeBuilder } from '@/datasources/staking-api/entities/__tests__/defi-vault-state.entity.builder';
4+
import { DefiVaultStakeSchema } from '@/datasources/staking-api/entities/defi-vault-stake.entity';
5+
6+
describe('DefiVaultStakeSchema', () => {
7+
it('should validate a DefiVaultStake', () => {
8+
const defiVaultStake = defiVaultStakeBuilder().build();
9+
10+
const result = DefiVaultStakeSchema.safeParse(defiVaultStake);
11+
12+
expect(result.success).toBe(true);
13+
});
14+
15+
it.each(['owner' as const, 'vault' as const])(
16+
'should checksum the %s address',
17+
(field) => {
18+
const lowerCaseAddress = faker.finance.ethereumAddress().toLowerCase();
19+
const defiVaultStake = defiVaultStakeBuilder()
20+
.with(field, lowerCaseAddress as `0x${string}`)
21+
.build();
22+
23+
const result = DefiVaultStakeSchema.safeParse(defiVaultStake);
24+
25+
expect(result.success && result.data[field]).toBe(
26+
getAddress(lowerCaseAddress),
27+
);
28+
},
29+
);
30+
31+
it.each([
32+
'current_balance' as const,
33+
'shares_balance' as const,
34+
'total_rewards' as const,
35+
'current_rewards' as const,
36+
'total_deposited_amount' as const,
37+
'total_withdrawn_amount' as const,
38+
])('should not allow a non-numerical string for %s', (field) => {
39+
const defiVaultStake = defiVaultStakeBuilder()
40+
.with(field, 'not-a-number')
41+
.build();
42+
43+
const result = DefiVaultStakeSchema.safeParse(defiVaultStake);
44+
45+
expect(!result.success && result.error.issues[0]).toStrictEqual({
46+
code: 'custom',
47+
message: 'Invalid base-10 numeric string',
48+
path: [field],
49+
});
50+
});
51+
52+
it('should default to unknown for unknown chain values', () => {
53+
const defiVaultStake = defiVaultStakeBuilder()
54+
.with('chain', faker.string.alpha() as 'unknown')
55+
.build();
56+
57+
const result = DefiVaultStakeSchema.safeParse(defiVaultStake);
58+
59+
expect(result.success && result.data.chain).toBe('unknown');
60+
});
61+
62+
it('should not validate a non-DefiVaultStake', () => {
63+
const defiVaultStake = {
64+
invalid: 'defiVaultStake',
65+
};
66+
67+
const result = DefiVaultStakeSchema.safeParse(defiVaultStake);
68+
69+
expect(!result.success && result.error.issues).toStrictEqual([
70+
{
71+
code: 'invalid_type',
72+
expected: 'string',
73+
message: 'Required',
74+
path: ['vault_id'],
75+
received: 'undefined',
76+
},
77+
{
78+
code: 'invalid_type',
79+
expected: 'string',
80+
message: 'Required',
81+
path: ['owner'],
82+
received: 'undefined',
83+
},
84+
{
85+
code: 'invalid_type',
86+
expected: 'string',
87+
message: 'Required',
88+
path: ['current_balance'],
89+
received: 'undefined',
90+
},
91+
{
92+
code: 'invalid_type',
93+
expected: 'string',
94+
message: 'Required',
95+
path: ['shares_balance'],
96+
received: 'undefined',
97+
},
98+
{
99+
code: 'invalid_type',
100+
expected: 'string',
101+
message: 'Required',
102+
path: ['total_rewards'],
103+
received: 'undefined',
104+
},
105+
{
106+
code: 'invalid_type',
107+
expected: 'string',
108+
message: 'Required',
109+
path: ['current_rewards'],
110+
received: 'undefined',
111+
},
112+
{
113+
code: 'invalid_type',
114+
expected: 'string',
115+
message: 'Required',
116+
path: ['total_deposited_amount'],
117+
received: 'undefined',
118+
},
119+
{
120+
code: 'invalid_type',
121+
expected: 'string',
122+
message: 'Required',
123+
path: ['total_withdrawn_amount'],
124+
received: 'undefined',
125+
},
126+
{
127+
code: 'invalid_type',
128+
expected: 'string',
129+
message: 'Required',
130+
path: ['vault'],
131+
received: 'undefined',
132+
},
133+
{
134+
code: 'invalid_type',
135+
expected: 'number',
136+
message: 'Required',
137+
path: ['chain_id'],
138+
received: 'undefined',
139+
},
140+
{
141+
code: 'invalid_type',
142+
expected: 'number',
143+
message: 'Required',
144+
path: ['updated_at_block'],
145+
received: 'undefined',
146+
},
147+
]);
148+
});
149+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { faker } from '@faker-js/faker';
2+
import { getAddress } from 'viem';
3+
4+
import { Builder } from '@/__tests__/builder';
5+
import { DefiVaultStatsChains } from '@/datasources/staking-api/entities/defi-vault-stats.entity';
6+
import type { IBuilder } from '@/__tests__/builder';
7+
import type { DefiVaultStake } from '@/datasources/staking-api/entities/defi-vault-stake.entity';
8+
9+
export function defiVaultStakeBuilder(): IBuilder<DefiVaultStake> {
10+
return new Builder<DefiVaultStake>()
11+
.with('vault_id', faker.string.uuid())
12+
.with('owner', getAddress(faker.finance.ethereumAddress()))
13+
.with('current_balance', faker.number.int({ min: 0, max: 100 }).toString())
14+
.with('shares_balance', faker.number.int({ min: 0, max: 100 }).toString())
15+
.with('total_rewards', faker.number.int({ min: 0, max: 100 }).toString())
16+
.with('current_rewards', faker.number.int({ min: 0, max: 100 }).toString())
17+
.with(
18+
'total_deposited_amount',
19+
faker.number.int({ min: 0, max: 100 }).toString(),
20+
)
21+
.with(
22+
'total_withdrawn_amount',
23+
faker.number.int({ min: 0, max: 100 }).toString(),
24+
)
25+
.with('vault', getAddress(faker.finance.ethereumAddress()))
26+
.with('chain', faker.helpers.arrayElement(DefiVaultStatsChains))
27+
.with('chain_id', faker.number.int({ min: 0, max: 100 }))
28+
.with('updated_at_block', faker.number.int({ min: 0, max: 100 }));
29+
}

src/datasources/staking-api/entities/__tests__/defi-vault-stats.entity.builder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export function defiVaultStatsBuilder(): IBuilder<DefiVaultStats> {
3535
.with('chain_id', faker.number.int())
3636
.with('asset_decimals', faker.number.int())
3737
.with('updated_at_block', faker.number.int())
38+
.with('additional_rewards_nrr', faker.number.float())
3839
.with(
3940
'additional_rewards',
4041
faker.helpers.multiple(() => defiVaultAdditionalRewardBuilder().build(), {

src/datasources/staking-api/entities/__tests__/defi-vault-stats.entity.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,13 @@ describe('DefiVaultStatsSchema', () => {
219219
path: ['updated_at_block'],
220220
received: 'undefined',
221221
},
222+
{
223+
code: 'invalid_type',
224+
expected: 'number',
225+
message: 'Required',
226+
path: ['additional_rewards_nrr'],
227+
received: 'undefined',
228+
},
222229
]);
223230
});
224231

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { z } from 'zod';
2+
import { AddressSchema } from '@/validation/entities/schemas/address.schema';
3+
import { NumericStringSchema } from '@/validation/entities/schemas/numeric-string.schema';
4+
5+
// Note: only the used subset of fields returned by Kiln
6+
export const DefiMorphoExtraRewardSchema = z.object({
7+
chain_id: z.number(),
8+
asset: AddressSchema,
9+
claimable: NumericStringSchema,
10+
claimable_next: NumericStringSchema,
11+
});
12+
13+
export const DefiMorphoExtraRewardsSchema = z.array(
14+
DefiMorphoExtraRewardSchema,
15+
);
16+
17+
export type DefiMorphoExtraReward = z.infer<typeof DefiMorphoExtraRewardSchema>;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { z } from 'zod';
2+
import { DefiVaultStatsSchema } from '@/datasources/staking-api/entities/defi-vault-stats.entity';
3+
import { AddressSchema } from '@/validation/entities/schemas/address.schema';
4+
import { NumericStringSchema } from '@/validation/entities/schemas/numeric-string.schema';
5+
6+
export const DefiVaultStakeSchema = z.object({
7+
vault_id: z.string(),
8+
owner: AddressSchema,
9+
current_balance: NumericStringSchema,
10+
shares_balance: NumericStringSchema,
11+
total_rewards: NumericStringSchema,
12+
current_rewards: NumericStringSchema,
13+
total_deposited_amount: NumericStringSchema,
14+
total_withdrawn_amount: NumericStringSchema,
15+
vault: AddressSchema,
16+
chain: DefiVaultStatsSchema.shape.chain,
17+
chain_id: z.number(),
18+
updated_at_block: z.number(),
19+
});
20+
21+
export const DefiVaultStakesSchema = z.array(DefiVaultStakeSchema);
22+
23+
export type DefiVaultStake = z.infer<typeof DefiVaultStakeSchema>;

src/datasources/staking-api/entities/defi-vault-stats.entity.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const DefiVaultStatsSchema = z.object({
4343
chain_id: z.number(),
4444
asset_decimals: z.number(),
4545
updated_at_block: z.number(),
46+
additional_rewards_nrr: z.number(),
4647
additional_rewards: z
4748
.array(DefiVaultStatsAdditionalRewardSchema)
4849
.nullish()

0 commit comments

Comments
 (0)