-
-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathState.ts
More file actions
142 lines (123 loc) · 4.27 KB
/
Copy pathState.ts
File metadata and controls
142 lines (123 loc) · 4.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { Balance, Transaction } from '@metamask/keyring-api';
import type { CaipAssetType } from '@metamask/utils';
import type { Address, Signature } from '@solana/kit';
import { unset } from 'lodash';
import type { SolanaKeyringAccount } from '../../../domain';
import type { SpotPrices } from '../../clients/price-api/types';
import { deserialize } from '../../serialization/deserialize';
import { serialize } from '../../serialization/serialize';
import type { Serializable } from '../../serialization/types';
import { safeMerge } from '../../utils/safeMerge';
import type { IStateManager } from './IStateManager';
export type AccountId = string;
export type UnencryptedStateValue = {
keyringAccounts: Record<string, SolanaKeyringAccount>;
mapInterfaceNameToId: Record<string, string>;
transactions: Record<AccountId, Transaction[]>;
// we need to store the exhaustive list of signatures (including spam)
// to keep track of the transactions per account. The field transactions above only stores non-spam transactions, which break the refreshAccounts cronjob logic.
signatures: Record<Address, Signature[]>;
assets: Record<AccountId, Record<CaipAssetType, Balance>>;
tokenPrices: SpotPrices;
};
export const DEFAULT_UNENCRYPTED_STATE: UnencryptedStateValue = {
keyringAccounts: {},
mapInterfaceNameToId: {},
transactions: {},
signatures: {},
assets: {},
tokenPrices: {},
};
export type StateConfig<TValue extends Record<string, Serializable>> = {
encrypted: boolean;
defaultState: TValue;
};
/**
* This class is a layer on top the the `snap_manageState` API that facilitates its usage:
*
* Basic usage:
* - Get and update the sate of the snap
*
* Serialization:
* - It serializes the data before storing it in the snap state because only JSON-assignable data can be stored.
* - It deserializes the data after retrieving it from the snap state.
* - So you don't need to worry about the data format when storing or retrieving data.
*
* Default values:
* - It merges the default state with the underlying snap state to ensure that we always have default values,
* letting us avoid a ton of null checks everywhere.
*/
export class State<TStateValue extends Record<string, Serializable>>
implements IStateManager<TStateValue>
{
#config: StateConfig<TStateValue>;
constructor(config: StateConfig<TStateValue>) {
this.#config = config;
}
async get(): Promise<TStateValue> {
const state = await snap.request({
method: 'snap_getState',
params: {
encrypted: this.#config.encrypted,
},
});
const stateDeserialized = deserialize(state ?? {}) as TStateValue;
// Merge the default state with the underlying snap state
// to ensure that we always have default values. It lets us avoid a ton of null checks everywhere.
const stateWithDefaults = safeMerge(
this.#config.defaultState,
stateDeserialized,
);
return stateWithDefaults;
}
async getKey<TResponse extends Serializable>(
key: string,
): Promise<TResponse | undefined> {
const value = await snap.request({
method: 'snap_getState',
params: {
key,
encrypted: this.#config.encrypted,
},
});
if (value === null) {
return undefined;
}
return deserialize(value) as TResponse;
}
async setKey(key: string, value: Serializable): Promise<void> {
await snap.request({
method: 'snap_setState',
params: {
key,
value: serialize(value),
encrypted: this.#config.encrypted,
},
});
}
async update(
updaterFunction: (state: TStateValue) => TStateValue,
): Promise<TStateValue> {
return this.get().then(async (state) => {
const newState = updaterFunction(state);
await snap.request({
method: 'snap_manageState',
params: {
operation: 'update',
newState: serialize(newState),
encrypted: this.#config.encrypted,
},
});
return newState;
});
}
async deleteKey(key: string): Promise<void> {
await this.update((state) => {
// Using lodash's unset to leverage the json path capabilities
unset(state, key);
return state;
});
}
}