@otoplo/wallet-common 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/index.d.ts +2278 -0
- package/dist/index.js +2005 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
- package/src/index.ts +5 -0
- package/src/persistence/datastore/db.ts +47 -0
- package/src/persistence/datastore/index.ts +2 -0
- package/src/persistence/datastore/kv.ts +129 -0
- package/src/persistence/index.ts +2 -0
- package/src/persistence/wallet-db.ts +251 -0
- package/src/services/asset.ts +220 -0
- package/src/services/cache.ts +54 -0
- package/src/services/discovery.ts +110 -0
- package/src/services/index.ts +8 -0
- package/src/services/key.ts +55 -0
- package/src/services/rostrum.ts +214 -0
- package/src/services/session.ts +225 -0
- package/src/services/transaction.ts +388 -0
- package/src/services/tx-transformer.ts +244 -0
- package/src/services/wallet.ts +650 -0
- package/src/state/hooks.ts +45 -0
- package/src/state/index.ts +3 -0
- package/src/state/slices/auth.ts +28 -0
- package/src/state/slices/dapp.ts +32 -0
- package/src/state/slices/index.ts +6 -0
- package/src/state/slices/loader.ts +31 -0
- package/src/state/slices/notifications.ts +44 -0
- package/src/state/slices/status.ts +70 -0
- package/src/state/slices/wallet.ts +112 -0
- package/src/state/store.ts +24 -0
- package/src/types/dapp.types.ts +21 -0
- package/src/types/db.types.ts +142 -0
- package/src/types/index.ts +5 -0
- package/src/types/notification.types.ts +13 -0
- package/src/types/rostrum.types.ts +161 -0
- package/src/types/wallet.types.ts +62 -0
- package/src/utils/asset.ts +103 -0
- package/src/utils/common.ts +159 -0
- package/src/utils/enums.ts +22 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/keypath.ts +15 -0
- package/src/utils/price.ts +40 -0
- package/src/utils/seed.ts +57 -0
- package/src/utils/vault.ts +39 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Address, BNExtended, BufferWriter, Script, ScriptOpcode } from "libnexa-ts";
|
|
2
|
+
import type { VaultDTO } from "../types";
|
|
3
|
+
import { AccountType, generateVaultAddress, KeySpace } from "../utils";
|
|
4
|
+
import type { KeyManager } from "./key";
|
|
5
|
+
import type { RostrumService } from "./rostrum";
|
|
6
|
+
|
|
7
|
+
const HODL_FIRST_BLOCK = 274710;
|
|
8
|
+
const HODL_SCRIPT_PREFIX = "0014461ad25081cb0119d034385ff154c8d3ad6bdd76";
|
|
9
|
+
|
|
10
|
+
export class WalletDiscoveryService {
|
|
11
|
+
|
|
12
|
+
private readonly rostrumService: RostrumService;
|
|
13
|
+
private readonly keyManager: KeyManager;
|
|
14
|
+
|
|
15
|
+
public constructor(rostrumService: RostrumService, keyManager: KeyManager) {
|
|
16
|
+
this.rostrumService = rostrumService;
|
|
17
|
+
this.keyManager = keyManager;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public async discoverWalletIndex(type: AccountType, keySpace: KeySpace): Promise<number> {
|
|
21
|
+
let index = 0, stop = false, addrBatch = 0;
|
|
22
|
+
|
|
23
|
+
do {
|
|
24
|
+
stop = true;
|
|
25
|
+
for (let i = addrBatch; i < addrBatch+20; i++) {
|
|
26
|
+
const keyPath = { account: type, type: keySpace, index: i };
|
|
27
|
+
const rAddr = this.keyManager.getKey(keyPath).privateKey.toAddress().toString();
|
|
28
|
+
const isUsed = await this.rostrumService.isAddressUsed(rAddr);
|
|
29
|
+
if (isUsed) {
|
|
30
|
+
index = i;
|
|
31
|
+
stop = false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
addrBatch += 20;
|
|
35
|
+
} while (!stop);
|
|
36
|
+
|
|
37
|
+
return index;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public async discoverVaults(addresses: string[]): Promise<Map<string, VaultDTO>> {
|
|
41
|
+
const vaultsPromises: Promise<Set<string>>[] = [];
|
|
42
|
+
for (const address of addresses) {
|
|
43
|
+
const p = this.checkVaultsForAddress(address);
|
|
44
|
+
vaultsPromises.push(p);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const res = await Promise.all(vaultsPromises);
|
|
48
|
+
const vaults = new Map<string, VaultDTO>();
|
|
49
|
+
|
|
50
|
+
res.forEach(set => {
|
|
51
|
+
set.forEach(hex => {
|
|
52
|
+
const vault = this.parseVaultDetails(hex);
|
|
53
|
+
if (vault) {
|
|
54
|
+
vaults.set(vault.address, vault);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return vaults;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async checkVaultsForAddress(address: string): Promise<Set<string>> {
|
|
63
|
+
const vaults = new Set<string>();
|
|
64
|
+
|
|
65
|
+
const history = await this.rostrumService.getTransactionsHistory(address, HODL_FIRST_BLOCK);
|
|
66
|
+
for (const txHistory of history) {
|
|
67
|
+
const tx = await this.rostrumService.getTransaction(txHistory.tx_hash);
|
|
68
|
+
const hodls = tx.vout.filter(out => out.scriptPubKey.hex.startsWith(HODL_SCRIPT_PREFIX));
|
|
69
|
+
for (const hodl of hodls) {
|
|
70
|
+
vaults.add(hodl.scriptPubKey.hex);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return vaults;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private parseVaultDetails(hex: string): VaultDTO | undefined {
|
|
78
|
+
const scirptTemplate = Script.fromHex(hex);
|
|
79
|
+
const buf = new BufferWriter().writeVarLengthBuf(scirptTemplate.toBuffer()).toBuffer();
|
|
80
|
+
const actualAddress = new Address(buf).toString();
|
|
81
|
+
|
|
82
|
+
const args = scirptTemplate.getVisibleArgs();
|
|
83
|
+
|
|
84
|
+
const block = BNExtended.fromScriptNumBuffer(args.chunks[0].buf!).toNumber();
|
|
85
|
+
const index = ScriptOpcode.isSmallIntOp(args.chunks[1].opcodenum)
|
|
86
|
+
? ScriptOpcode.decodeOP_N(args.chunks[1].opcodenum)
|
|
87
|
+
: BNExtended.fromScriptNumBuffer(args.chunks[1].buf!).toNumber();
|
|
88
|
+
|
|
89
|
+
const visibleArgs = [block, index];
|
|
90
|
+
const key = this.keyManager.getKey({ account: AccountType.VAULT, type: KeySpace.RECEIVE, index: index });
|
|
91
|
+
const expectedAddress = generateVaultAddress(key.publicKey, visibleArgs);
|
|
92
|
+
|
|
93
|
+
if (actualAddress != expectedAddress) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const dto: VaultDTO = {
|
|
98
|
+
address: actualAddress,
|
|
99
|
+
block: block,
|
|
100
|
+
idx: index,
|
|
101
|
+
statusHash: '',
|
|
102
|
+
balance: { confirmed: "0", unconfirmed: "0" },
|
|
103
|
+
tokensBalance: {},
|
|
104
|
+
height: 0,
|
|
105
|
+
type: AccountType.VAULT
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return dto;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { mnemonicToSeedSync } from "@scure/bip39";
|
|
2
|
+
import { HDPrivateKey } from "libnexa-ts";
|
|
3
|
+
import type { AccountType } from "../utils/enums";
|
|
4
|
+
import type { KeyPath } from "../types/wallet.types";
|
|
5
|
+
import { keyPathToString, stringToKeyPath } from "../utils/keypath";
|
|
6
|
+
|
|
7
|
+
export class KeyManager {
|
|
8
|
+
|
|
9
|
+
private seed!: Uint8Array;
|
|
10
|
+
private masterKey!: HDPrivateKey;
|
|
11
|
+
private accountKeys = new Map<number, HDPrivateKey>();
|
|
12
|
+
private walletKeys = new Map<string, HDPrivateKey>();
|
|
13
|
+
|
|
14
|
+
public init(mnemonic: string | Uint8Array): void {
|
|
15
|
+
this.seed = typeof mnemonic === "string" ? mnemonicToSeedSync(mnemonic) : mnemonic;
|
|
16
|
+
this.masterKey = HDPrivateKey.fromSeed(this.seed).deriveChild(44, true).deriveChild(29223, true);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public reset(): void {
|
|
20
|
+
if (!this.seed) {
|
|
21
|
+
throw new Error("KeysManager not initialized");
|
|
22
|
+
}
|
|
23
|
+
this.masterKey = HDPrivateKey.fromSeed(this.seed).deriveChild(44, true).deriveChild(29223, true);
|
|
24
|
+
this.accountKeys.clear();
|
|
25
|
+
this.walletKeys.clear();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private getAccountKey(account: AccountType): HDPrivateKey {
|
|
29
|
+
let key = this.accountKeys.get(account);
|
|
30
|
+
if (!key) {
|
|
31
|
+
key = this.masterKey.deriveChild(account, true);
|
|
32
|
+
this.accountKeys.set(account, key);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return key;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public getKey(keyPath: string | KeyPath): HDPrivateKey {
|
|
39
|
+
const path = typeof keyPath === 'string'
|
|
40
|
+
? keyPath
|
|
41
|
+
: keyPathToString(keyPath.account, keyPath.type, keyPath.index);
|
|
42
|
+
|
|
43
|
+
let key = this.walletKeys.get(path);
|
|
44
|
+
if (!key) {
|
|
45
|
+
const { account, type, index } = typeof keyPath === 'string'
|
|
46
|
+
? stringToKeyPath(keyPath)
|
|
47
|
+
: keyPath;
|
|
48
|
+
const accountKey = this.getAccountKey(account);
|
|
49
|
+
key = accountKey.deriveChild(type, false).deriveChild(index, false);
|
|
50
|
+
this.walletKeys.set(path, key);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return key;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { RequestResponse} from "@otoplo/electrum-client";
|
|
2
|
+
import { ConnectionStatus, ElectrumClient, TransportScheme } from "@otoplo/electrum-client";
|
|
3
|
+
import type { BlockTip, IFirstUse, IListUnspentRecord, ITokenGenesis, ITokenListUnspent, ITokensBalance, ITokenUtxo, ITransaction, ITXHistory, IUtxo, RostrumParams, ServerFeatures } from "../types/rostrum.types";
|
|
4
|
+
import type { Balance } from "../types/wallet.types";
|
|
5
|
+
import { isTestnet } from "../utils/common";
|
|
6
|
+
import type { KVStore } from "../persistence/datastore/kv";
|
|
7
|
+
|
|
8
|
+
type RPCParameter = object | string | number | boolean | null;
|
|
9
|
+
|
|
10
|
+
export class RostrumService {
|
|
11
|
+
|
|
12
|
+
private readonly kvStore: KVStore;
|
|
13
|
+
|
|
14
|
+
private client?: ElectrumClient;
|
|
15
|
+
|
|
16
|
+
public constructor(kvStore: KVStore) {
|
|
17
|
+
this.kvStore = kvStore;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public getFeatures(): Promise<ServerFeatures> {
|
|
21
|
+
return this.execute<ServerFeatures>('server.features');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public getBlockTip(): Promise<BlockTip> {
|
|
25
|
+
return this.execute<BlockTip>('blockchain.headers.tip');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public getBalance(address: string): Promise<Balance> {
|
|
29
|
+
return this.execute<Balance>('blockchain.address.get_balance', address, 'exclude_tokens');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public getTransactionsHistory(address: string, fromHeight = 0): Promise<ITXHistory[]> {
|
|
33
|
+
return this.execute<ITXHistory[]>('blockchain.address.get_history', address, { from_height: fromHeight });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public getFirstUse(address: string): Promise<IFirstUse> {
|
|
37
|
+
return this.execute<IFirstUse>('blockchain.address.get_first_use', address);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public async isAddressUsed(address: string): Promise<boolean> {
|
|
41
|
+
try {
|
|
42
|
+
const firstUse = await this.getFirstUse(address);
|
|
43
|
+
return !!(firstUse.tx_hash && firstUse.tx_hash !== "");
|
|
44
|
+
} catch (e) {
|
|
45
|
+
if (e instanceof Error && e.message.includes("not found")) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
throw e;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public getTransaction(id: string, verbose = true): Promise<ITransaction> {
|
|
53
|
+
return this.execute<ITransaction>('blockchain.transaction.get', id, verbose);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public getUtxo(outpoint: string): Promise<IUtxo> {
|
|
57
|
+
return this.execute<IUtxo>('blockchain.utxo.get', outpoint);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public getNexaUtxos(address: string): Promise<IListUnspentRecord[]> {
|
|
61
|
+
return this.execute<IListUnspentRecord[]>('blockchain.address.listunspent', address, 'exclude_tokens');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public async getTokenUtxos(address: string, token: string): Promise<ITokenUtxo[]> {
|
|
65
|
+
const listunspent = await this.execute<ITokenListUnspent>('token.address.listunspent', address, null, token);
|
|
66
|
+
return listunspent.unspent;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public async getTokensBalance(address: string): Promise<Record<string, Balance>> {
|
|
70
|
+
const tokensBalance = await this.execute<ITokensBalance>('token.address.get_balance', address);
|
|
71
|
+
const balance: Record<string, Balance> = {};
|
|
72
|
+
|
|
73
|
+
for (const cToken in tokensBalance.confirmed) {
|
|
74
|
+
if (tokensBalance.confirmed[cToken] != 0) {
|
|
75
|
+
balance[cToken] = { confirmed: BigInt(tokensBalance.confirmed[cToken]).toString(), unconfirmed: "0" }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const uToken in tokensBalance.unconfirmed) {
|
|
80
|
+
if (tokensBalance.unconfirmed[uToken] != 0) {
|
|
81
|
+
if (balance[uToken]) {
|
|
82
|
+
balance[uToken].unconfirmed = BigInt(tokensBalance.unconfirmed[uToken]).toString();
|
|
83
|
+
} else {
|
|
84
|
+
balance[uToken] = { confirmed: "0", unconfirmed: BigInt(tokensBalance.unconfirmed[uToken]).toString() }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return balance;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public getTokenGenesis(token: string): Promise<ITokenGenesis> {
|
|
93
|
+
return this.execute<ITokenGenesis>('token.genesis.info', token);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public broadcast(txHex: string): Promise<string> {
|
|
97
|
+
return this.execute<string>('blockchain.transaction.broadcast', txHex);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public subscribeHeaders(handler: (block: number) => void): Promise<Error | RequestResponse> {
|
|
101
|
+
return this.client!.subscribe((response: any) => {
|
|
102
|
+
const data = Array.isArray(response) ? response[0] : response;
|
|
103
|
+
const height = typeof data?.height === 'number' ? data.height : 0;
|
|
104
|
+
handler(height);
|
|
105
|
+
}, 'blockchain.headers.subscribe');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public subscribeAddress(address: string, handler: (data: unknown) => void): Promise<Error | RequestResponse> {
|
|
109
|
+
return this.client!.subscribe(handler, 'blockchain.address.subscribe', address);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public async getLatency(): Promise<number> {
|
|
113
|
+
try {
|
|
114
|
+
const start = Date.now();
|
|
115
|
+
const res = await this.getBlockTip();
|
|
116
|
+
if (res) {
|
|
117
|
+
return Date.now() - start;
|
|
118
|
+
}
|
|
119
|
+
return 0;
|
|
120
|
+
} catch {
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public async connect(params?: RostrumParams): Promise<void> {
|
|
126
|
+
try {
|
|
127
|
+
if (!params) {
|
|
128
|
+
params = await this.getCurrentInstance();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.client = new ElectrumClient("com.otoplo.wallet", "1.4.3", params.host, params.port, params.scheme, 45*1000, 10*1000);
|
|
132
|
+
await this.client.connect();
|
|
133
|
+
} catch (e) {
|
|
134
|
+
if (e instanceof Error) {
|
|
135
|
+
console.info(e.message);
|
|
136
|
+
} else {
|
|
137
|
+
console.error(e);
|
|
138
|
+
}
|
|
139
|
+
throw e;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public async disconnect(force?: boolean): Promise<boolean> {
|
|
144
|
+
try {
|
|
145
|
+
return await this.client?.disconnect(force) ?? false;
|
|
146
|
+
} catch (e) {
|
|
147
|
+
console.error(e)
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async execute<T>(method: string, ...parameters: RPCParameter[]): Promise<T> {
|
|
153
|
+
await this.waitForConnection();
|
|
154
|
+
const res = await this.client!.request(method, ...parameters);
|
|
155
|
+
if (res instanceof Error) {
|
|
156
|
+
throw res;
|
|
157
|
+
}
|
|
158
|
+
return res as T;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private waitForConnection(timeout = 5000): Promise<void> {
|
|
162
|
+
const start = Date.now();
|
|
163
|
+
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
const check = (): void => {
|
|
166
|
+
if (this.client?.connectionStatus == ConnectionStatus.CONNECTED) {
|
|
167
|
+
return resolve();
|
|
168
|
+
}
|
|
169
|
+
if (Date.now() - start > timeout) {
|
|
170
|
+
return reject(new Error("Rostrum Connection timeout"));
|
|
171
|
+
}
|
|
172
|
+
setTimeout(check, 250);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
check();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public async getCurrentInstance(): Promise<RostrumParams> {
|
|
180
|
+
const params = await this.kvStore.getRostrumParams();
|
|
181
|
+
if (params) {
|
|
182
|
+
return params;
|
|
183
|
+
}
|
|
184
|
+
return RostrumService.getPredefinedInstances()[0];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public static getPredefinedInstances(): RostrumParams[] {
|
|
188
|
+
if (isTestnet()) {
|
|
189
|
+
return [
|
|
190
|
+
{
|
|
191
|
+
scheme: TransportScheme.WSS,
|
|
192
|
+
host: 'testnet-electrum.nexa.org',
|
|
193
|
+
port: 30004,
|
|
194
|
+
label: 'NexaOrg'
|
|
195
|
+
}
|
|
196
|
+
];
|
|
197
|
+
} else {
|
|
198
|
+
return [
|
|
199
|
+
{
|
|
200
|
+
scheme: TransportScheme.WSS,
|
|
201
|
+
host: 'rostrum.otoplo.com',
|
|
202
|
+
port: 443,
|
|
203
|
+
label: 'Otoplo'
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
scheme: TransportScheme.WSS,
|
|
207
|
+
host: 'electrum.nexa.org',
|
|
208
|
+
port: 20004,
|
|
209
|
+
label: 'NexaOrg'
|
|
210
|
+
}
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { DAppProvider } from "wallet-comms-sdk";
|
|
2
|
+
import type { SessionInfo, AccountDTO, KeyPath, AppNotification, DappRpcRequest } from "../types";
|
|
3
|
+
import type { WalletDB } from "../persistence";
|
|
4
|
+
import { AccountType, KeySpace, MAIN_WALLET_ID, SessionRequestType } from "../utils";
|
|
5
|
+
import { Networks } from "libnexa-ts";
|
|
6
|
+
import type { KeyManager } from "./key";
|
|
7
|
+
|
|
8
|
+
type MethodHandler = {
|
|
9
|
+
resolve: (value: any) => void;
|
|
10
|
+
reject: (value: any) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type SessionEvent =
|
|
14
|
+
| { type: 'session_added'; accountId: number; sessionInfo: SessionInfo }
|
|
15
|
+
| { type: 'session_removed'; accountId: number; sessionId: string }
|
|
16
|
+
| { type: 'sessions_cleared', accountId: number }
|
|
17
|
+
| { type: 'new_notification'; notification: AppNotification }
|
|
18
|
+
| { type: 'new_request'; request: DappRpcRequest };
|
|
19
|
+
|
|
20
|
+
export type SessionUpdateCallback = (event: SessionEvent) => void;
|
|
21
|
+
|
|
22
|
+
export class SessionManager {
|
|
23
|
+
|
|
24
|
+
private readonly walletDb: WalletDB;
|
|
25
|
+
private readonly keyManager: KeyManager;
|
|
26
|
+
|
|
27
|
+
private readonly providers: Map<number, Map<string, DAppProvider>>;
|
|
28
|
+
private readonly handlers: Map<string, MethodHandler>;
|
|
29
|
+
|
|
30
|
+
private removeOnClose: boolean;
|
|
31
|
+
|
|
32
|
+
private updateCallback?: SessionUpdateCallback;
|
|
33
|
+
|
|
34
|
+
private constructor(walletDb: WalletDB, keyManager: KeyManager) {
|
|
35
|
+
this.walletDb = walletDb;
|
|
36
|
+
this.keyManager = keyManager;
|
|
37
|
+
this.providers = new Map();
|
|
38
|
+
this.handlers = new Map();
|
|
39
|
+
this.removeOnClose = true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public onUpdate(callback: SessionUpdateCallback): void {
|
|
43
|
+
this.updateCallback = callback;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private notify(event: SessionEvent): void {
|
|
47
|
+
this.updateCallback?.(event);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public getHandler(request: string): MethodHandler | undefined {
|
|
51
|
+
return this.handlers.get(request);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public add(account: AccountDTO, provider: DAppProvider, sessionInfo: SessionInfo): void {
|
|
55
|
+
if (!this.providers.has(account.id)) {
|
|
56
|
+
this.providers.set(account.id, new Map());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const sessionId = sessionInfo.details.sessionId;
|
|
60
|
+
const accountSessions = this.providers.get(account.id)!;
|
|
61
|
+
|
|
62
|
+
this.registerHandlers(account, provider, sessionId);
|
|
63
|
+
accountSessions.set(sessionId, provider);
|
|
64
|
+
this.notify({ type: 'session_added', accountId: account.id, sessionInfo });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public remove(accountId: number, sessionId: string): void {
|
|
68
|
+
const sessionMap = this.providers.get(accountId);
|
|
69
|
+
if (sessionMap) {
|
|
70
|
+
const provider = sessionMap.get(sessionId);
|
|
71
|
+
if (provider) {
|
|
72
|
+
provider.disconnect();
|
|
73
|
+
sessionMap.delete(sessionId);
|
|
74
|
+
}
|
|
75
|
+
if (sessionMap.size === 0) {
|
|
76
|
+
this.providers.delete(accountId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
this.notify({ type: 'session_removed', accountId, sessionId });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public async reload(accounts: Map<number, AccountDTO>): Promise<void> {
|
|
83
|
+
for (const account of accounts.values()) {
|
|
84
|
+
if (account.id == MAIN_WALLET_ID) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const storedSessions = await this.walletDb.getAccountSessions(account.id);
|
|
88
|
+
const currentSessions = this.providers.get(account.id);
|
|
89
|
+
|
|
90
|
+
const loadedSessions = storedSessions.map(async (session) => {
|
|
91
|
+
if (currentSessions?.has(session.sessionId)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let provider;
|
|
96
|
+
try {
|
|
97
|
+
provider = new DAppProvider(session.uri);
|
|
98
|
+
const details = provider.getSessionInfo();
|
|
99
|
+
await provider.connect(3000);
|
|
100
|
+
const appInfo = await provider.getAppInfo(2000);
|
|
101
|
+
await provider.joinSession(account.address, 2000);
|
|
102
|
+
const messages = await provider.fetchPendingMessages();
|
|
103
|
+
for (const msg of messages) {
|
|
104
|
+
this.notify({
|
|
105
|
+
type: 'new_notification',
|
|
106
|
+
notification: {
|
|
107
|
+
id: crypto.randomUUID(),
|
|
108
|
+
createdAt: msg.createdAt,
|
|
109
|
+
type: 'web3',
|
|
110
|
+
title: 'Request pending approval',
|
|
111
|
+
message: `A connected dApp (${appInfo.name}) has requested an action from your Account: ${account.name}. Review the request details before approving or rejecting.`,
|
|
112
|
+
action: {
|
|
113
|
+
type: 'DAPP_REQUEST',
|
|
114
|
+
account: account.id,
|
|
115
|
+
sessionId: details.sessionId,
|
|
116
|
+
payload: msg.message
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return { account: account, provider, metadata: { details, appInfo } };
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(`Failed to reload session ${session.sessionId}`, error);
|
|
124
|
+
provider?.disconnect();
|
|
125
|
+
await this.walletDb.removeSession(session.sessionId);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const results= await Promise.allSettled(loadedSessions);
|
|
130
|
+
for (const result of results) {
|
|
131
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
132
|
+
this.add(result.value.account, result.value.provider, result.value.metadata);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public clear(): void {
|
|
139
|
+
try {
|
|
140
|
+
this.removeOnClose = false;
|
|
141
|
+
for (const [accountId, sessions] of this.providers) {
|
|
142
|
+
for (const [,provider] of sessions) {
|
|
143
|
+
provider.disconnect();
|
|
144
|
+
}
|
|
145
|
+
sessions.clear();
|
|
146
|
+
this.notify({ type: 'sessions_cleared', accountId });
|
|
147
|
+
}
|
|
148
|
+
this.providers.clear();
|
|
149
|
+
} finally {
|
|
150
|
+
this.removeOnClose = true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public async revoke(accountId: number, sessionId: string): Promise<void> {
|
|
155
|
+
const sessionMap = this.providers.get(accountId);
|
|
156
|
+
if (sessionMap) {
|
|
157
|
+
const provider = sessionMap.get(sessionId);
|
|
158
|
+
if (provider) {
|
|
159
|
+
await provider.revokeSession();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public async replayMessage(accountId: number, sessionId: string, payload: string): Promise<void> {
|
|
165
|
+
const sessionMap = this.providers.get(accountId);
|
|
166
|
+
if (sessionMap) {
|
|
167
|
+
const provider = sessionMap.get(sessionId);
|
|
168
|
+
if (provider) {
|
|
169
|
+
await provider.replayMessage(payload);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private registerHandlers(account: AccountDTO, provider: DAppProvider, sessionId: string): void {
|
|
175
|
+
provider.onSessionDelete((): Promise<void> => {
|
|
176
|
+
return this.walletDb.removeSession(sessionId);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
provider.onClose((): void => {
|
|
180
|
+
if (this.removeOnClose) {
|
|
181
|
+
this.remove(account.id, sessionId);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const handleRequest = <T>(type: SessionRequestType, request: unknown): Promise<T> => {
|
|
186
|
+
return new Promise<T>((resolve, reject) => {
|
|
187
|
+
this.handlers.set(type, { resolve, reject });
|
|
188
|
+
this.notify({ type: 'new_request', request: { type, accountId: account.id, sessionId, request } });
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
provider.onSignMessage(signMsgReq => {
|
|
193
|
+
return handleRequest(SessionRequestType.SignMessage, signMsgReq);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
provider.onAddToken(addTokenReq => {
|
|
197
|
+
return handleRequest(SessionRequestType.AddToken, addTokenReq);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
provider.onSendTransaction(sendTransactionReq => {
|
|
201
|
+
return handleRequest(SessionRequestType.SendTransaction, sendTransactionReq);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
provider.onSignTransaction(signTransactionReq => {
|
|
205
|
+
return handleRequest(SessionRequestType.SignTransaction, signTransactionReq);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
provider.onGetAccount(() => {
|
|
209
|
+
const path: KeyPath = { account: AccountType.DAPP, type: KeySpace.RECEIVE, index: account.id };
|
|
210
|
+
return {
|
|
211
|
+
name: account.name,
|
|
212
|
+
address: account.address,
|
|
213
|
+
pubkey: this.keyManager.getKey(path).publicKey.toString(),
|
|
214
|
+
blockchain: "nexa",
|
|
215
|
+
network: Networks.defaultNetwork.name,
|
|
216
|
+
capabilities: {
|
|
217
|
+
addToken: true,
|
|
218
|
+
sendTransaction: true,
|
|
219
|
+
signMessage: true,
|
|
220
|
+
signTransaction: true
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|