-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
phan-wallet-mock.ts
376 lines (340 loc) 路 10.5 KB
/
phan-wallet-mock.ts
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
import {
clusterApiUrl,
Commitment,
ConfirmedTransaction,
Connection,
ConnectionConfig,
Finality,
Keypair,
LAMPORTS_PER_SOL,
RpcResponseAndContext,
SignatureResult,
Signer,
Transaction,
} from '@solana/web3.js'
export * from './types'
import { EventEmitter } from 'eventemitter3'
import { PhantomWallet, PhantomWalletEvents, TransactionSummary } from './types'
import assert_module from 'assert'
const assert: typeof import('assert') = assert_module.strict ?? assert_module
import nacl from 'tweetnacl'
import debug from 'debug'
import { forceUint8Array } from './utils'
import {
partialSign,
TransactionWithInternals,
verifySignatures,
} from './web3js'
import bs58 from 'bs58'
const logInfo = debug('phan:info')
const logDebug = debug('phan:debug')
const logError = debug('phan:error')
export const DEVNET = clusterApiUrl('devnet')
export const TESTNET = clusterApiUrl('testnet')
export const MAINNET_BETA = clusterApiUrl('mainnet-beta')
export const LOCALNET = 'http://127.0.0.1:8899'
/**
* Standin for the the [https://phantom.app/ | phantom wallet] to use while testing.
* Behaves as much as possible as the original which is why care needs to be taken when using it.
*
* The main difference is that no user confirmation is required to approve a
* transaction or signature.
*
* This means that user approval is automatic!
*
*/
export class PhantomWalletMock
extends EventEmitter<PhantomWalletEvents>
implements PhantomWallet
{
readonly isPhantom = true
private _signer: Signer
private _connection: Connection | undefined
private _transactionSignatures: string[] = []
private constructor(
private readonly _connectionURL: string,
private _keypair: Keypair,
private readonly _commitmentOrConfig?: Commitment | ConnectionConfig
) {
super()
logInfo('Initializing Phan Wallet Mock: %o', {
cluster: _connectionURL,
pubkey: _keypair.publicKey.toBase58(),
commitment: _commitmentOrConfig,
})
this._signer = this._signerFromKeypair(this._keypair)
}
_signerFromKeypair(keypair: Keypair) {
return {
publicKey: keypair.publicKey,
secretKey: forceUint8Array(keypair.secretKey),
}
}
// -----------------
// Get info/properties
// -----------------
/**
* The underlying connection to a fullnode JSON RPC endpoint
*/
get connection(): Connection {
assert(
this._connection != null,
'Need to connect first before getting connection'
)
return this._connection
}
/**
* `true` if the wallet was connected via {@link PhantomWalletMock#connect}
*/
get isConnected(): boolean {
return this._connection != null
}
/**
* Public key of the currently used wallet.
*/
get publicKey() {
return this._keypair.publicKey
}
/**
* Secret key of the currently used wallet.
* @category TestAPI
*/
get secretKey() {
return this._keypair.secretKey.toString()
}
/**
* {@link Signer} of the currently used wallet.
*/
get signer(): Signer {
return this._signer
}
/**
* Connection URL used for the underlying connection.
* @category TestAPI
*/
get connectionURL() {
return this._connectionURL
}
/**
* Default commitment used for transactions.
* @category TestAPI
*/
get commitment(): Commitment | undefined {
const comm = this._commitmentOrConfig
if (comm == null) return undefined
return typeof comm === 'string' ? comm : comm.commitment
}
/**
* All transactions signatures made since wallet was created ordered oldest to most recent.
* @category TestAPI
*/
get transactionSignatures(): string[] {
return Array.from(this._transactionSignatures)
}
/**
* Fetch the balance for the wallet's account.
* @category TestAPI
*/
getBalance(commitment: Commitment = 'confirmed') {
return this.connection.getBalance(this.publicKey, commitment)
}
/**
* Fetch the balance in Sol for the wallet's account.
* @category TestAPI
*/
async getBalanceSol(commitment: Commitment = 'confirmed') {
const lamports = await this.getBalance(commitment)
return lamports / LAMPORTS_PER_SOL
}
/**
* Fetches transaction details for the last confirmed transaction signed with this wallet.
* @category TestAPI
*/
getLastConfirmedTransaction(
commitment?: Finality
): Promise<null | ConfirmedTransaction> {
const lastSig = this._transactionSignatures.pop()
if (lastSig == null) {
logDebug('No transaction signature found')
return Promise.resolve(null)
}
return this.connection.getConfirmedTransaction(lastSig, commitment)
}
/**
* Fetches transaction details for the last confirmed transaction signed with this wallet and
* returns its summary.
* @category TestAPI
*/
async lastConfirmedTransactionSummary(
commitment?: Finality
): Promise<TransactionSummary | undefined> {
const tx = await this.getLastConfirmedTransaction(commitment)
if (tx == null) return
const logMessages = tx.meta?.logMessages ?? []
const fee = tx.meta?.fee
const slot = tx.slot
const blockTime = tx.blockTime ?? 0
const err = tx.meta?.err
return { logMessages, fee, slot, blockTime, err }
}
/**
* Fetches transaction details for the last confirmed transaction signed with this wallet and
* returns its summary that can be used to log it to the console.
* @category TestAPI
*/
async lastConfirmedTransactionString(commitment?: Finality): Promise<string> {
const tx = await this.getLastConfirmedTransaction(commitment)
if (tx == null) {
return 'No confirmed transaction found'
}
const logs = tx.meta?.logMessages?.join('\n ')
const fee = tx.meta?.fee
const slot = tx.slot
const blockTimeSecs = new Date(tx.blockTime ?? 0).getSeconds()
const err = tx.meta?.err
return `fee: ${fee} slot: ${slot} blockTime: ${blockTimeSecs}s err: ${err}
${logs}`
}
// -----------------
// Signing Transactions
// -----------------
/**
* Signs the transaction using the current wallet.
*/
signTransaction(txIn: Transaction): Promise<Transaction> {
const transaction: TransactionWithInternals =
txIn as TransactionWithInternals
transaction._partialSign = partialSign.bind(transaction)
transaction._verifySignatures = verifySignatures.bind(transaction)
logDebug(
'Attempting to sign transaction with %d instruction(s)',
transaction.instructions.length
)
return new Promise(async (resolve, reject) => {
try {
assert(this._connection != null, 'Need to connect wallet first')
const { blockhash } = await this._connection.getRecentBlockhash()
transaction.recentBlockhash = blockhash
transaction.sign(this._signer)
logDebug('Signed transaction successfully')
this._transactionSignatures.push(bs58.encode(transaction.signature!))
resolve(transaction)
} catch (err) {
logError('Failed signing transaction')
logError(err)
reject(err)
}
})
}
/**
* Signs all transactions using the current wallet.
*/
signAllTransactions(transactions: Transaction[]): Promise<Transaction[]> {
logDebug('Signing %d transactions', transactions.length)
return Promise.all(transactions.map((tx) => this.signTransaction(tx)))
}
// -----------------
// Signing Message
// -----------------
/**
* Signs the message using the current wallet.
*/
signMessage(message: Uint8Array): Promise<{ signature: Uint8Array }> {
return new Promise(async (resolve, reject) => {
try {
assert(this._connection != null, 'Need to connect wallet first')
const signature = nacl.sign.detached(
forceUint8Array(message),
this._keypair.secretKey
)
const res = {
signature,
publicKey: this._keypair.publicKey,
}
return resolve(res)
} catch (err) {
logError('Failed signing message')
logError(err)
reject(err)
}
})
}
// -----------------
// Managing Connection
// -----------------
/**
* Connects the wallet to the URL provided on wallet creation.
* This needs to be called before attempting to sign messages or transactions.
*
* emits 'connect'
*/
async connect(): Promise<void> {
this._connection = new Connection(
this._connectionURL,
this._commitmentOrConfig
)
logDebug('wallet connected')
this.emit('connect')
return Promise.resolve()
}
/**
* Disconnects the wallet.
*
* emits 'disconnect'
*/
disconnect(): Promise<void> {
this._connection = undefined
logDebug('wallet disconnected')
this._handleDisconnect()
return Promise.resolve()
}
_handleDisconnect(...args: unknown[]): unknown {
return this.emit('disconnect', args)
}
// -----------------
// Added convenience API for Testing purposes
// -----------------
/**
* Drops sol to the currently connected wallet.
* @category TestAPI
*/
async requestAirdrop(
sol: number
): Promise<RpcResponseAndContext<SignatureResult>> {
assert(this._connection != null, 'Need to connect requesting airdrop')
const signature = await this._connection.requestAirdrop(
this.publicKey,
LAMPORTS_PER_SOL * sol
)
return this.connection.confirmTransaction(signature)
}
/**
* Changes the Wallet to the provided keypair
* This updates the signer as well.
* @category TestAPI
*/
changeWallet(keypair: Keypair) {
this._keypair = keypair
this._signer = this._signerFromKeypair(keypair)
}
/**
* Creates a {@see PhantomWalletMock} instance with the provided info.
*
* @param connectionURL cluster to connect to, i.e. `https://api.devnet.solana.com` or `http://127.0.0.1:8899`
* @param keypair the private and public key of the wallet to use, i.e. the payer/signer
* @param commitmentOrConfig passed to the {@link * https://solana-labs.github.io/solana-web3.js/classes/Connection.html#constructor }
* when creating a connection to the cluster
*/
static create = (
connectionURL: string,
keypair: Keypair = Keypair.generate(),
commitmentOrConfig?: Commitment | ConnectionConfig
) => new PhantomWalletMock(connectionURL, keypair, commitmentOrConfig)
}
export const initWalletMockProvider = (winin: Window) => {
const win = winin as Window & { solana: PhantomWallet | undefined }
const payer = Keypair.generate()
const wallet = PhantomWalletMock.create(LOCALNET, payer, 'confirmed')
win.solana = wallet
return { wallet, payer }
}