Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "Remove fallback for Kiln's unverified contracts (#1919)" #1922

Merged
merged 1 commit into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
271 changes: 271 additions & 0 deletions src/routes/transactions/transactions-view.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,100 @@ describe('TransactionsViewController tests', () => {
});
});

it('returns the native staking `deposit` confirmation view using local decoding', async () => {
const chain = chainBuilder().with('isTestnet', false).build();
const deployment = deploymentBuilder()
.with('chain_id', +chain.chainId)
.with('product_type', 'dedicated')
.with('product_fee', faker.number.float().toString())
.build();
const dedicatedStakingStats = dedicatedStakingStatsBuilder().build();
const networkStats = networkStatsBuilder().build();
const safeAddress = faker.finance.ethereumAddress();
const data = encodeFunctionData({
abi: parseAbi(['function deposit() external payable']),
});
const value = getNumberString(64 * 10 ** 18 + 1);
networkService.get.mockImplementation(({ url }) => {
switch (url) {
case `${safeConfigUrl}/api/v1/chains/${chain.chainId}`:
return Promise.resolve({ data: chain, status: 200 });
case `${stakingApiUrl}/v1/deployments`:
return Promise.resolve({
data: { data: [deployment] },
status: 200,
});
case `${stakingApiUrl}/v1/eth/kiln-stats`:
return Promise.resolve({
data: { data: dedicatedStakingStats },
status: 200,
});
case `${stakingApiUrl}/v1/eth/network-stats`:
return Promise.resolve({
data: { data: networkStats },
status: 200,
});
default:
return Promise.reject(new Error(`Could not match ${url}`));
}
});
networkService.post.mockImplementation(({ url }) => {
if (url === `${chain.transactionService}/api/v1/data-decoder/`) {
return Promise.reject(new ServiceUnavailableException());
}
return Promise.reject(new Error(`Could not match ${url}`));
});

const annualNrr =
dedicatedStakingStats.gross_apy.last_30d *
(1 - Number(deployment.product_fee));
const monthlyNrr = annualNrr / 12;
const expectedAnnualReward = (annualNrr / 100) * Number(value);
const expectedMonthlyReward = expectedAnnualReward / 12;
const expectedFiatAnnualReward =
(expectedAnnualReward * networkStats.eth_price_usd) /
Math.pow(10, chain.nativeCurrency.decimals);
const expectedFiatMonthlyReward = expectedFiatAnnualReward / 12;

await request(app.getHttpServer())
.post(
`/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`,
)
.send({
to: deployment.address,
data,
value,
})
.expect(200)
.expect({
type: 'KILN_NATIVE_STAKING_DEPOSIT',
method: 'deposit',
status: 'SIGNATURE_NEEDED',
parameters: [],
estimatedEntryTime: networkStats.estimated_entry_time_seconds,
estimatedExitTime: networkStats.estimated_exit_time_seconds,
estimatedWithdrawalTime:
networkStats.estimated_withdrawal_time_seconds,
fee: +deployment.product_fee!,
monthlyNrr,
annualNrr,
value,
numValidators: 2,
expectedAnnualReward: getNumberString(expectedAnnualReward),
expectedMonthlyReward: getNumberString(expectedMonthlyReward),
expectedFiatAnnualReward,
expectedFiatMonthlyReward,
tokenInfo: {
address: NULL_ADDRESS,
decimals: chain.nativeCurrency.decimals,
logoUri: chain.nativeCurrency.logoUri,
name: chain.nativeCurrency.name,
symbol: chain.nativeCurrency.symbol,
trusted: true,
},
});
});

it('returns the dedicated staking `deposit` confirmation view from batch', async () => {
const chain = chainBuilder().with('isTestnet', false).build();
const dataDecoded = dataDecodedBuilder().build();
Expand Down Expand Up @@ -1220,6 +1314,105 @@ describe('TransactionsViewController tests', () => {
});
});

it('returns the native staking `validators exit` confirmation view using local decoding', async () => {
const chain = chainBuilder().with('isTestnet', false).build();
const deployment = deploymentBuilder()
.with('chain_id', +chain.chainId)
.with('product_type', 'dedicated')
.with('product_fee', faker.number.float().toString())
.build();
const safeAddress = faker.finance.ethereumAddress();
const networkStats = networkStatsBuilder().build();
const validatorPublicKey = faker.string.hexadecimal({
length: KilnDecoder.KilnPublicKeyLength,
});
const data = encodeFunctionData({
abi: parseAbi(['function requestValidatorsExit(bytes)']),
functionName: 'requestValidatorsExit',
args: [validatorPublicKey as `0x${string}`],
});
const stakes = [
stakeBuilder().with('rewards', '4000000').build(),
stakeBuilder().with('rewards', '2000000').build(),
];
networkService.get.mockImplementation(({ url }) => {
switch (url) {
case `${safeConfigUrl}/api/v1/chains/${chain.chainId}`:
return Promise.resolve({ data: chain, status: 200 });
case `${stakingApiUrl}/v1/deployments`:
return Promise.resolve({
data: { data: [deployment] },
status: 200,
});
case `${stakingApiUrl}/v1/eth/network-stats`:
return Promise.resolve({
data: { data: networkStats },
status: 200,
});
case `${stakingApiUrl}/v1/eth/stakes`:
return Promise.resolve({
data: { data: stakes },
status: 200,
});
default:
return Promise.reject(new Error(`Could not match ${url}`));
}
});
networkService.post.mockImplementation(({ url }) => {
if (url === `${chain.transactionService}/api/v1/data-decoder/`) {
return Promise.reject(new ServiceUnavailableException());
}
return Promise.reject(new Error(`Could not match ${url}`));
});

await request(app.getHttpServer())
.post(
`/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`,
)
.send({
to: deployment.address,
data,
})
.expect(200)
.expect({
type: 'KILN_NATIVE_STAKING_VALIDATORS_EXIT',
method: 'requestValidatorsExit',
parameters: [
{
name: '_publicKeys',
type: 'bytes',
value: validatorPublicKey.toLowerCase(),
valueDecoded: null,
},
],
status: 'SIGNATURE_NEEDED',
estimatedExitTime: networkStats.estimated_exit_time_seconds,
estimatedWithdrawalTime:
networkStats.estimated_withdrawal_time_seconds,
value: '32000000000000000000',
numValidators: 1,
rewards: '6000000',
tokenInfo: {
address: NULL_ADDRESS,
decimals: chain.nativeCurrency.decimals,
logoUri: chain.nativeCurrency.logoUri,
name: chain.nativeCurrency.name,
symbol: chain.nativeCurrency.symbol,
trusted: true,
},
});

// check the public keys are passed to the staking service in the expected format
expect(networkService.get).toHaveBeenNthCalledWith(4, {
url: `${stakingApiUrl}/v1/eth/stakes`,
networkRequest: expect.objectContaining({
params: {
validators: `${validatorPublicKey.toLowerCase()}`,
},
}),
});
});

it('returns the generic confirmation view if the deployment is not available', async () => {
const chain = chainBuilder().with('isTestnet', false).build();
const dataDecoded = dataDecodedBuilder().build();
Expand Down Expand Up @@ -1663,6 +1856,84 @@ describe('TransactionsViewController tests', () => {
});
});

it('returns the native staking `withdraw` confirmation view using local decoding', async () => {
const chain = chainBuilder().with('isTestnet', false).build();
const validatorPublicKey = faker.string.hexadecimal({
length: KilnDecoder.KilnPublicKeyLength * 2,
}); // 2 validators
const deployment = deploymentBuilder()
.with('chain_id', +chain.chainId)
.with('product_type', 'dedicated')
.with('product_fee', faker.number.float().toString())
.build();
const safeAddress = faker.finance.ethereumAddress();
const data = encodeFunctionData({
abi: parseAbi(['function batchWithdrawCLFee(bytes)']),
functionName: 'batchWithdrawCLFee',
args: [`${validatorPublicKey}` as `0x${string}`],
});
const stakes = [
stakeBuilder().with('rewards', '6000000').build(),
stakeBuilder().with('rewards', '2000000').build(),
];
networkService.get.mockImplementation(({ url }) => {
switch (url) {
case `${safeConfigUrl}/api/v1/chains/${chain.chainId}`:
return Promise.resolve({ data: chain, status: 200 });
case `${stakingApiUrl}/v1/deployments`:
return Promise.resolve({
data: { data: [deployment] },
status: 200,
});
case `${stakingApiUrl}/v1/eth/stakes`:
return Promise.resolve({
data: { data: stakes },
status: 200,
});
default:
return Promise.reject(new Error(`Could not match ${url}`));
}
});
networkService.post.mockImplementation(({ url }) => {
if (url === `${chain.transactionService}/api/v1/data-decoder/`) {
return Promise.reject(new ServiceUnavailableException());
}
return Promise.reject(new Error(`Could not match ${url}`));
});

await request(app.getHttpServer())
.post(
`/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`,
)
.send({
to: deployment.address,
data,
})
.expect(200)
.expect({
type: 'KILN_NATIVE_STAKING_WITHDRAW',
method: 'batchWithdrawCLFee',
parameters: [
{
name: '_publicKeys',
type: 'bytes',
value: validatorPublicKey.toLowerCase(),
valueDecoded: null,
},
],
value: '64000000000000000000',
rewards: '8000000',
tokenInfo: {
address: NULL_ADDRESS,
decimals: chain.nativeCurrency.decimals,
logoUri: chain.nativeCurrency.logoUri,
name: chain.nativeCurrency.name,
symbol: chain.nativeCurrency.symbol,
trusted: true,
},
});
});

it('returns the generic confirmation view if the deployment is not available', async () => {
const chain = chainBuilder().with('isTestnet', false).build();
const validatorPublicKey = faker.string.hexadecimal();
Expand Down
3 changes: 2 additions & 1 deletion src/routes/transactions/transactions-view.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DataDecodedRepositoryModule } from '@/domain/data-decoder/data-decoded.repository.interface';
import { KilnDecoder } from '@/domain/staking/contracts/decoders/kiln-decoder.helper';
import { ComposableCowDecoder } from '@/domain/swaps/contracts/decoders/composable-cow-decoder.helper';
import { GPv2DecoderModule } from '@/domain/swaps/contracts/decoders/gp-v2-decoder.helper';
import { SwapsRepositoryModule } from '@/domain/swaps/swaps-repository.module';
Expand Down Expand Up @@ -97,7 +98,7 @@ export class TransactionsViewController {
SwapsRepositoryModule,
TwapOrderHelperModule,
],
providers: [TransactionsViewService, ComposableCowDecoder],
providers: [TransactionsViewService, ComposableCowDecoder, KilnDecoder],
controllers: [TransactionsViewController],
})
export class TransactionsViewControllerModule {}
Loading