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

Persist derived sponsor wallets #71

Merged
merged 12 commits into from
Nov 7, 2023
1 change: 1 addition & 0 deletions src/state/state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const stateMock = {
],
},
},
derivedSponsorWallets: {},
dapis: {},
};

Expand Down
12 changes: 11 additions & 1 deletion src/state/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ import type { BigNumber } from 'ethers';
import { produce, type Draft } from 'immer';

import type { Config } from '../config/schema';
import type { ChainId, DApiName, DecodedDataFeed, DataFeedId, SignedData, Provider } from '../types';
import type {
DapiName,
PrivateKey,
DecodedDataFeed,
ChainId,
SignedData,
DataFeedId,
Provider,
DApiName,
} from '../types';

interface GasState {
gasPrices: { price: BigNumber; timestampMs: number }[];
Expand Down Expand Up @@ -30,6 +39,7 @@ export interface State {
config: Config;
dataFetcherInterval?: NodeJS.Timeout;
gasPriceStore: Record<string, Record<string, GasState>>;
derivedSponsorWallets: Record<DapiName, PrivateKey>;
signedApiStore: Record<DataFeedId, SignedData>;
signedApiUrlStore: Record<ChainId, Record<Provider, string[]>>;
dapis: Record<DApiName, DapiState>;
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { type EvmAddress, evmAddressSchema, type EvmId, evmIdSchema } from './co

export type AirnodeAddress = EvmAddress;
export type TemplateId = EvmId;
export type DapiName = string;
export type PrivateKey = string;
export type DataFeedId = EvmId;
export type ChainId = string;
export type DApiName = string;
Expand Down
21 changes: 21 additions & 0 deletions src/update-feeds/update-transactions.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { Api3ServerV1 } from '@api3/airnode-protocol-v1';
import { ethers } from 'ethers';

import { init } from '../../test/fixtures/mock-config';
import { generateMockApi3ServerV1 } from '../../test/fixtures/mock-contract';
import { getState, updateState } from '../state';

import {
deriveSponsorWallet,
Expand Down Expand Up @@ -60,12 +62,31 @@ describe(estimateMulticallGasLimit.name, () => {

describe(deriveSponsorWallet.name, () => {
it('derives sponsor wallets for a dAPI', () => {
init();

const btcEthDapiName = ethers.utils.formatBytes32String('BTC/ETH');
const sponsorWalletMnemonic = 'diamond result history offer forest diagram crop armed stumble orchard stage glance';

const btcEthSponsorWallet = deriveSponsorWallet(sponsorWalletMnemonic, btcEthDapiName);

expect(btcEthSponsorWallet.address).toBe('0xDa8b0388F435F609C8cdA6cf73C890D90205c863');

expect(getState().derivedSponsorWallets).toStrictEqual({
'0x4254432f45544800000000000000000000000000000000000000000000000000':
'0xb69b0c7c2623257b72b020d13044f86eb2a4a1f41c994cfd5198c95bb2f7de7c',
});

const usdEthDapiName = ethers.utils.formatBytes32String('USD/ETH');
updateState((draft) => {
draft.derivedSponsorWallets[usdEthDapiName] =
'0xbc4a90108f885d06e7b72b713f9c02890b363795b63cd21d4561aeba3379205c';
});

const otherSponsorWallet = deriveSponsorWallet(sponsorWalletMnemonic, usdEthDapiName);

expect(otherSponsorWallet._signingKey().privateKey).toBe(
'0xbc4a90108f885d06e7b72b713f9c02890b363795b63cd21d4561aeba3379205c'
);
});
});

Expand Down
16 changes: 15 additions & 1 deletion src/update-feeds/update-transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ethers } from 'ethers';
import { AIRSEEKER_PROTOCOL_ID } from '../constants';
import { getAirseekerRecommendedGasPrice } from '../gas-price/gas-price';
import { logger } from '../logger';
import { getState } from '../state';
import { getState, updateState } from '../state';
import type { SignedData, ChainId, Provider } from '../types';

import type { ReadDapiWithIndexResponse } from './dapi-data-registry';
Expand Down Expand Up @@ -143,6 +143,13 @@ export const estimateMulticallGasLimit = async (
};

export const deriveSponsorWallet = (sponsorWalletMnemonic: string, dapiName: string) => {
const { derivedSponsorWallets } = getState();

const privateKey = derivedSponsorWallets?.[dapiName];
if (privateKey) {
return new ethers.Wallet(privateKey);
}

// Take first 20 bytes of dapiName as sponsor address together with the "0x" prefix.
const sponsorAddress = ethers.utils.getAddress(dapiName.slice(0, 42));
const sponsorWallet = ethers.Wallet.fromMnemonic(
Expand All @@ -151,6 +158,13 @@ export const deriveSponsorWallet = (sponsorWalletMnemonic: string, dapiName: str
);
logger.debug('Derived sponsor wallet', { sponsorAddress, sponsorWalletAddress: sponsorWallet.address });

updateState((draft) => {
draft.derivedSponsorWallets = {
...draft.derivedSponsorWallets,
[dapiName]: sponsorWallet.privateKey,
};
});
aquarat marked this conversation as resolved.
Show resolved Hide resolved

return sponsorWallet;
};

Expand Down
6 changes: 5 additions & 1 deletion test/e2e/update-feeds.feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ it('updates blockchain data', async () => {
walletFunder,
} = await deployAndUpdate();
const btcDapi = await dapiDataRegistry.readDapiWithIndex(0);

const decodedDataFeed = decodeDataFeed(btcDapi.dataFeed);
const decodedBtcDapi = { ...omit(btcDapi, ['dataFeed']), decodedDataFeed };

const currentBlock = await dapiDataRegistry.provider.getBlock('latest');
const currentBlockTimestamp = currentBlock.timestamp;
const binanceBtcSignedData = await generateSignedData(
Expand Down Expand Up @@ -82,5 +84,7 @@ it('updates blockchain data', async () => {
},
]);

expect(logger.debug).toHaveBeenCalledWith('Successfully updated dAPI');
expect(logger.debug).toHaveBeenNthCalledWith(1, 'Estimating gas limit');
expect(logger.debug).toHaveBeenNthCalledWith(2, 'Deriving sponsor wallet');
expect(logger.debug).toHaveBeenNthCalledWith(5, 'Successfully updated dAPI');
});
1 change: 1 addition & 0 deletions test/fixtures/mock-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const init = (stateOverride?: Partial<State>) => {
gasPriceStore: {},
signedApiStore: {},
signedApiUrlStore: {},
derivedSponsorWallets: {},
dapis: {},
...stateOverride,
});
Expand Down