-
Notifications
You must be signed in to change notification settings - Fork 68
/
Copy pathwormhole.ts
486 lines (448 loc) · 16.8 KB
/
wormhole.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
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
import type { Chain, Network, Platform } from "@wormhole-foundation/sdk-base";
import { chainToPlatform, circle } from "@wormhole-foundation/sdk-base";
import type {
ChainAddress,
ChainContext,
Contracts,
NativeAddress,
PayloadDiscriminator,
PayloadLiteral,
PlatformContext,
PlatformUtils,
TokenAddress,
TokenId,
TxHash,
UniversalAddress,
WormholeMessageId,
deserialize,
} from "@wormhole-foundation/sdk-definitions";
import {
canonicalAddress,
isNative,
nativeTokenId,
toNative,
} from "@wormhole-foundation/sdk-definitions";
import { getCircleAttestationWithRetry } from "./circle-api.js";
import type { WormholeConfig, WormholeConfigOverrides } from "./config.js";
import { applyWormholeConfigOverrides } from "./config.js";
import { DEFAULT_TASK_TIMEOUT } from "./config.js";
import { CircleTransfer } from "./protocols/cctp/cctpTransfer.js";
import { TokenTransfer } from "./protocols/tokenBridge/tokenTransfer.js";
import type { RouteConstructor } from "./routes/index.js";
import { RouteResolver } from "./routes/resolver.js";
import { retry } from "./tasks.js";
import type { RelayStatus, TransactionStatus } from "./whscan-api.js";
import {
getIsVaaEnqueued,
getRelayStatus,
getTransactionStatusWithRetry,
getTxsByAddress,
getVaaByTxHashWithRetry,
getVaaBytesWithRetry,
getVaaWithRetry,
} from "./whscan-api.js";
type PlatformMap<N extends Network, P extends Platform = Platform> = Map<P, PlatformContext<N, P>>;
type ChainMap<N extends Network, C extends Chain = Chain> = Map<C, ChainContext<N, C>>;
export class Wormhole<N extends Network> {
protected readonly _network: N;
protected _platforms: PlatformMap<N>;
protected _chains: ChainMap<N>;
readonly config: WormholeConfig<N>;
constructor(network: N, platforms: PlatformUtils<any>[], config?: WormholeConfigOverrides<N>) {
this._network = network;
this.config = applyWormholeConfigOverrides(network, config) as WormholeConfig<N>;
this._chains = new Map();
this._platforms = new Map();
for (const p of platforms) {
this._platforms.set(p._platform, new p(network, this.config.chains));
}
}
get network(): N {
return this._network;
}
/**
* Creates a CircleTransfer object to move Native USDC from one chain to another
* @param amount the amount to transfer
* @param from the address to transfer from
* @param to the address to transfer to
* @param automatic whether to use automatic delivery
* @param payload the payload to send with the transfer
* @param nativeGas the amount of native gas to send with the transfer
* @returns the CircleTransfer object
* @throws Errors if the chain or protocol is not supported
*/
async circleTransfer(
amount: bigint,
from: ChainAddress,
to: ChainAddress,
automatic: boolean,
payload?: Uint8Array,
nativeGas?: bigint,
): Promise<CircleTransfer<N>> {
if (automatic && payload) throw new Error("Payload with automatic delivery is not supported");
if (
!circle.isCircleChain(this.network, from.chain) ||
!circle.isCircleChain(this.network, to.chain) ||
!circle.isCircleSupported(this.network, from.chain) ||
!circle.isCircleSupported(this.network, to.chain)
)
throw new Error(`Network and chain not supported: ${this.network} ${from.chain} `);
// ensure the amount is > fee + native gas
if (automatic) {
const acb = await this.getChain(from.chain).getAutomaticCircleBridge();
const relayerFee = await acb.getRelayerFee(to.chain);
const minAmount = relayerFee + (nativeGas ? nativeGas : 0n);
if (amount < minAmount)
throw new Error(`Amount must be > ${minAmount} (relayerFee + nativeGas)`);
}
return await CircleTransfer.from(this, {
amount,
from,
to,
automatic,
payload,
nativeGas,
});
}
/**
* Creates a TokenTransfer object to move a token from one chain to another
* @param token the token to transfer
* @param amount the amount to transfer
* @param from the address to transfer from
* @param to the address to transfer to
* @param automatic whether to use automatic delivery
* @param payload the payload to send with the transfer
* @param nativeGas the amount of native gas to send with the transfer
* @returns the TokenTransfer object
* @throws Errors if the chain or protocol is not supported
*/
async tokenTransfer(
token: TokenId,
amount: bigint,
from: ChainAddress,
to: ChainAddress,
automatic: boolean,
payload?: Uint8Array,
nativeGas?: bigint,
): Promise<TokenTransfer<N>> {
return await TokenTransfer.from(this, {
token,
amount,
from,
to,
automatic,
payload,
nativeGas,
});
}
/**
* Gets a RouteResolver configured with the routes passed
* @param routes the list RouteConstructors to use
* @returns the RouteResolver
*/
resolver(routes: RouteConstructor[]) {
return new RouteResolver(this, routes);
}
/**
* Gets the contract addresses for a given chain
* @param chain the chain name
* @returns the contract addresses
*/
getContracts(chain: Chain): Contracts | undefined {
return this.config.chains[chain]?.contracts;
}
/**
* Returns the platform object, i.e. the class with platform-specific logic and methods
* @param chain the platform name
* @returns the platform context class
* @throws Errors if platform is not found
*/
getPlatform<P extends Platform>(platformName: P): PlatformContext<N, P> {
const platform = this._platforms.get(platformName);
if (!platform)
throw new Error(
`Not able to retrieve platform ${platformName}. Did it get registered in the constructor?`,
);
return platform as PlatformContext<N, P>;
}
/**
* Returns the chain "context", i.e. the class with chain-specific logic and methods
* @param chain the chain name
* @returns the chain context class
* @throws Errors if context is not found
*/
getChain<C extends Chain>(chain: C): ChainContext<N, C> {
const platform = chainToPlatform(chain);
if (!this._chains.has(chain))
this._chains.set(chain, this.getPlatform(platform).getChain(chain));
return this._chains.get(chain)! as ChainContext<N, C>;
}
/**
* Gets the TokenId for a token representation on any chain
* These are the Wormhole wrapped token addresses, not necessarily
* the canonical version of that token
*
* @param chain The chain name to get the wrapped token address
* @param tokenId The Token ID (chain/address) of the original token
* @returns The TokenId on the given chain, null if it does not exist
* @throws Errors if the chain is not supported or the token does not exist
*/
async getWrappedAsset<C extends Chain>(chain: C, token: TokenId<Chain>): Promise<TokenId<C>> {
const ctx = this.getChain(chain);
const tb = await ctx.getTokenBridge();
return { chain, address: await tb.getWrappedAsset(token) };
}
/**
* Taking the original TokenId for some wrapped token chain
* These are the Wormhole wrapped token addresses, not necessarily
* the canonical version of that token
*
* @param tokenId The Token ID of the token we're looking up the original asset for
* @returns The Original TokenId corresponding to the token id passed,
* @throws Errors if the chain is not supported or the token does not exist
*/
async getOriginalAsset<C extends Chain>(token: TokenId<C>): Promise<TokenId<Chain>> {
const ctx = this.getChain(token.chain);
const tb = await ctx.getTokenBridge();
return await tb.getOriginalAsset(token.address);
}
/**
* Returns the UniversalAddress of the token. This may require fetching on-chain data.
* @param chain The chain to get the UniversalAddress for
* @param token The address to get the UniversalAddress for
* @returns The UniversalAddress of the token
*/
async getTokenUniversalAddress<C extends Chain>(
chain: C,
token: NativeAddress<C>,
): Promise<UniversalAddress> {
const ctx = this.getChain(chain);
const tb = await ctx.getTokenBridge();
return await tb.getTokenUniversalAddress(token);
}
/**
* Returns the native address of the token. This may require fetching on-chain data.
* @param chain The chain to get the native address for
* @param originChain The chain the token is from / native to
* @param token The address to get the native address for
* @returns The native address of the token
*/
async getTokenNativeAddress<C extends Chain>(
chain: C,
originChain: Chain,
token: UniversalAddress,
): Promise<NativeAddress<C>> {
const ctx = this.getChain(chain);
const tb = await ctx.getTokenBridge();
return await tb.getTokenNativeAddress(originChain, token);
}
/**
* Gets the number of decimals for a token on a given chain
*
* @param chain The chain name or id of the token/representation
* @param token The token address
* @returns The number of decimals
*/
async getDecimals<C extends Chain>(chain: C, token: TokenAddress<C>): Promise<number> {
const ctx = this.getChain(chain);
return await ctx.getDecimals(token);
}
/**
* Fetches the balance of a given token for a wallet
*
* @param walletAddress The wallet address
* @param tokenId The token ID (its home chain and address on the home chain)
* @param chain The chain name or id
* @returns The token balance of the wormhole asset as a BigNumber
*/
async getBalance<C extends Chain>(
chain: C,
token: TokenAddress<C>,
walletAddress: string,
): Promise<bigint | null> {
const ctx = this.getChain(chain);
return ctx.getBalance(walletAddress, token);
}
/**
* Gets the associated token account for chains that require it (only Solana currently).
*
* @param token the TokenId of the token to get the token account for
* @param recipient the address of the primary account that may require a separate token account
* @returns
*/
async getTokenAccount<C extends Chain>(
recipient: ChainAddress<C>,
token: TokenId<C>,
): Promise<ChainAddress<C>> {
return this.getChain(recipient.chain).getTokenAccount(recipient.address, token.address);
}
/**
* Gets the Raw VAA Bytes from the API or Guardian RPC, finality must be met before the VAA will be available.
*
* @param wormholeMessageId The WormholeMessageId corresponding to the VAA to be fetched
* @param timeout The total amount of time to wait for the VAA to be available
* @returns The VAA bytes if available
* @throws Errors if the VAA is not available after the retries
*/
async getVaaBytes(
wormholeMessageId: WormholeMessageId,
timeout: number = DEFAULT_TASK_TIMEOUT,
): Promise<Uint8Array | null> {
return await getVaaBytesWithRetry(this.config.api, wormholeMessageId, timeout);
}
/**
* Gets a VAA from the API or Guardian RPC, finality must be met before the VAA will be available.
*
* @param id The WormholeMessageId or Transaction hash corresponding to the VAA to be fetched
* @param decodeAs The VAA type to decode the bytes as
* @param timeout The total amount of time to wait for the VAA to be available
* @returns The VAA if available
* @throws Errors if the VAA is not available after the retries
*/
async getVaa<T extends PayloadLiteral | PayloadDiscriminator>(
id: WormholeMessageId | TxHash,
decodeAs: T,
timeout: number = DEFAULT_TASK_TIMEOUT,
): Promise<ReturnType<typeof deserialize<T>> | null> {
if (typeof id === "string")
return await getVaaByTxHashWithRetry(this.config.api, id, decodeAs, timeout);
return await getVaaWithRetry(this.config.api, id, decodeAs, timeout);
}
/**
* Gets the RelayStatus for a given WormholeMessageId
*
* @param wormholeMessageId The WormholeMessageId corresponding to the relay status to be fetched
* @returns The RelayStatus if available otherwise null
*/
async getRelayStatus(wormholeMessageId: WormholeMessageId): Promise<RelayStatus | null> {
return await getRelayStatus(this.config.api, wormholeMessageId);
}
/**
* Gets if the token bridge transfer VAA has been enqueued by the Governor.
* @param id The WormholeMessageId corresponding to the token bridge transfer VAA to check
* @returns True if the transfer has been enqueued, false otherwise
*/
async getIsVaaEnqueued(id: WormholeMessageId): Promise<boolean> {
return await getIsVaaEnqueued(this.config.api, id);
}
/**
* Gets the CircleAttestation corresponding to the message hash logged in the transfer transaction.
* @param msgHash The keccak256 hash of the message emitted by the circle contract
* @param timeout The total amount of time to wait for the VAA to be available
* @returns The CircleAttestation as a string, if available
* @throws Errors if the CircleAttestation is not available after the retries
*/
async getCircleAttestation(
msgHash: string,
timeout: number = DEFAULT_TASK_TIMEOUT,
): Promise<string | null> {
return getCircleAttestationWithRetry(this.config.circleAPI, msgHash, timeout);
}
/**
* Get the status of a transaction, identified by the chain, emitter address, and sequence number
*
* @param id the message id for the Wormhole Message to get transaction status for or originating Transaction hash
* @returns the TransactionStatus
*/
async getTransactionStatus(
id: WormholeMessageId | TxHash,
timeout = DEFAULT_TASK_TIMEOUT,
): Promise<TransactionStatus | null> {
let msgid: WormholeMessageId;
// No txid endpoint exists to get the status by txhash yet
if (typeof id === "string") {
const vaa = await getVaaByTxHashWithRetry(this.config.api, id, "Uint8Array", timeout);
if (!vaa) return null;
msgid = { emitter: vaa.emitterAddress, chain: vaa.emitterChain, sequence: vaa.sequence };
} else {
msgid = id;
}
return await getTransactionStatusWithRetry(this.config.api, msgid, timeout);
}
/**
* Get recent transactions for an address
*
* @param address the string formatted address to get transactions for
* @returns the TransactionStatus
*/
async getTransactionsForAddress(
address: string,
pageSize: number = 50,
page: number = 0,
): Promise<TransactionStatus[] | null> {
return getTxsByAddress(this.config.api, address, pageSize, page);
}
/**
* Parse an address from its canonical string format to a NativeAddress
*
* @param chain The chain the address is for
* @param address The address in canonical string format
* @returns The address in the NativeAddress format
*/
static parseAddress<C extends Chain>(chain: C, address: string): NativeAddress<C> {
return toNative(chain, address);
}
/**
* Return a string in the canonical chain format representing the address
* of a token or account
*
* @param chainAddress The ChainAddress or TokenId to get a string address
* @returns The string address in canonical format for the chain
*/
static canonicalAddress(chainAddress: ChainAddress | TokenId): string {
return canonicalAddress(chainAddress);
}
/**
* Parse an address from its canonical string format to a NativeAddress
*
* @param chain The chain the address is for
* @param address The native address in canonical string format
* @returns The ChainAddress
*/
static chainAddress<C extends Chain>(chain: C, address: string): ChainAddress<C> {
return { chain, address: Wormhole.parseAddress(chain, address) };
}
/**
* Parse an address from its canonical string format to a NativeAddress
*
* @param chain The chain the address is for
* @param address The native address in canonical string format or the string "native"
* @returns The ChainAddress
*/
static tokenId<C extends Chain>(chain: C, address: string): TokenId<C> {
return isNative(address) ? nativeTokenId(chain) : this.chainAddress(chain, address);
}
/**
* Parses all relevant information from a transaction given the sending tx hash and sending chain
*
* @param chain The sending chain name or context
* @param tx The sending transaction hash
* @returns The parsed WormholeMessageId
*/
static async parseMessageFromTx<N extends Network, C extends Chain>(
chain: ChainContext<N, C>,
txid: TxHash,
timeout: number = DEFAULT_TASK_TIMEOUT,
): Promise<WormholeMessageId[]> {
const task = async () => {
try {
const msgs = await chain.parseTransaction(txid);
// possible the node we hit does not have this data yet
// return null to signal retry
if (msgs.length === 0) return null;
return msgs;
} catch (e) {
console.error(e);
return null;
}
};
const parsed = await retry<WormholeMessageId[]>(
task,
chain.config.blockTime,
timeout,
"WormholeCore:ParseMessageFromTransaction",
);
if (!parsed) throw new Error(`No WormholeMessageId found for ${txid}`);
return parsed;
}
}