Skip to content

Commit c6dfcf9

Browse files
committed
Added manual TBTC route
1 parent e81584e commit c6dfcf9

40 files changed

+3642
-31
lines changed

connect/src/routes/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from "./common.js";
77
export * from "./tokenBridge/index.js";
88
export * from "./portico/index.js";
99
export * from "./cctp/index.js";
10+
export * from "./tbtc/index.js";

connect/src/routes/tbtc/README.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
The TBTCRoute enables the transfer of both native and Wormhole-wrapped Ethereum TBTC.
2+
3+
Arbitrum, Base, Optimism, Polygon, and Solana have a gateway contract. This contract mints native TBTC when it receives a payload3 transfer of Wormhole-wrapped TBTC from the token bridge. Conversely, it burns native TBTC when bridging out. Wormhole-wrapped TBTC serves as the "highway" asset for bridging TBTC between chains.
4+
Transfers of TBTC to chains without a gateway contract are regular token bridge transfers.
5+
6+
You can view the EVM L2WormholeGateway contract code [here](https://github.com/keep-network/tbtc-v2/blob/main/solidity/contracts/l2/L2WormholeGateway.sol). The Solana program is [here](https://github.com/keep-network/tbtc-v2/tree/main/cross-chain/solana).

connect/src/routes/tbtc/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./tbtc.js";

connect/src/routes/tbtc/tbtc.ts

+331
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
import {
2+
amount,
3+
Chain,
4+
contracts,
5+
finality,
6+
guardians,
7+
Network,
8+
} from "@wormhole-foundation/sdk-base";
9+
import { ManualRoute, StaticRouteMethods } from "../route.js";
10+
import {
11+
ChainAddress,
12+
ChainContext,
13+
deserialize,
14+
isSameToken,
15+
serialize,
16+
Signer,
17+
TBTCBridge,
18+
TokenId,
19+
TransactionId,
20+
WormholeMessageId,
21+
} from "@wormhole-foundation/sdk-definitions";
22+
import {
23+
Options,
24+
Quote,
25+
QuoteResult,
26+
Receipt,
27+
TransferParams,
28+
ValidatedTransferParams,
29+
ValidationResult,
30+
} from "../types.js";
31+
import {
32+
AttestedTransferReceipt,
33+
CompletedTransferReceipt,
34+
isAttested,
35+
isSourceFinalized,
36+
isSourceInitiated,
37+
TransferState,
38+
type AttestationReceipt,
39+
type SourceInitiatedTransferReceipt,
40+
type TransferReceipt,
41+
} from "../../types.js";
42+
import { RouteTransferRequest } from "../request.js";
43+
import { Wormhole } from "../../wormhole.js";
44+
import { signSendWait } from "../../common.js";
45+
46+
export namespace TBTCRoute {
47+
export type NormalizedParams = {
48+
amount: amount.Amount;
49+
};
50+
51+
export interface ValidatedParams extends ValidatedTransferParams<Options> {
52+
normalizedParams: NormalizedParams;
53+
}
54+
}
55+
56+
type Op = Options;
57+
type Vp = TBTCRoute.ValidatedParams;
58+
59+
type Tp = TransferParams<Op>;
60+
type Vr = ValidationResult<Op>;
61+
62+
type QR = QuoteResult<Op, Vp>;
63+
type Q = Quote<Op, Vp>;
64+
type R = TransferReceipt<AttestationReceipt<"TBTCBridge">>;
65+
66+
export class TBTCRoute<N extends Network>
67+
extends ManualRoute<N>
68+
implements StaticRouteMethods<typeof TBTCRoute>
69+
{
70+
static meta = {
71+
name: "TBTCBridge",
72+
};
73+
74+
static supportedNetworks(): Network[] {
75+
return ["Mainnet"];
76+
}
77+
78+
static supportedChains(network: Network): Chain[] {
79+
return contracts.tokenBridgeChains(network);
80+
}
81+
82+
static async supportedDestinationTokens<N extends Network>(
83+
sourceToken: TokenId,
84+
fromChain: ChainContext<N>,
85+
toChain: ChainContext<N>,
86+
): Promise<TokenId[]> {
87+
if (!(await this.isSourceTokenSupported(sourceToken, fromChain))) {
88+
return [];
89+
}
90+
91+
const tbtcToken = TBTCBridge.getNativeTbtcToken(toChain.chain);
92+
if (tbtcToken) {
93+
return [tbtcToken];
94+
}
95+
96+
const tb = await toChain.getTokenBridge();
97+
const ethTbtc = TBTCBridge.getNativeTbtcToken("Ethereum")!;
98+
try {
99+
const wrappedTbtc = await tb.getWrappedAsset(ethTbtc);
100+
return [Wormhole.tokenId(toChain.chain, wrappedTbtc.toString())];
101+
} catch (e: any) {
102+
if (e.message.includes("not a wrapped asset")) return [];
103+
throw e;
104+
}
105+
}
106+
107+
getDefaultOptions(): Op {
108+
return {};
109+
}
110+
111+
async validate(request: RouteTransferRequest<N>, params: Tp): Promise<Vr> {
112+
const amount = request.parseAmount(params.amount);
113+
114+
const validatedParams: Vp = {
115+
normalizedParams: {
116+
amount,
117+
},
118+
options: params.options ?? this.getDefaultOptions(),
119+
...params,
120+
};
121+
122+
return { valid: true, params: validatedParams };
123+
}
124+
125+
async quote(request: RouteTransferRequest<N>, params: Vp): Promise<QR> {
126+
const eta =
127+
finality.estimateFinalityTime(request.fromChain.chain) + guardians.guardianAttestationEta;
128+
129+
return {
130+
success: true,
131+
params,
132+
sourceToken: {
133+
token: request.source.id,
134+
amount: params.normalizedParams.amount,
135+
},
136+
destinationToken: {
137+
token: request.destination.id,
138+
amount: params.normalizedParams.amount,
139+
},
140+
eta,
141+
};
142+
}
143+
144+
async initiate(
145+
request: RouteTransferRequest<N>,
146+
signer: Signer,
147+
quote: Q,
148+
to: ChainAddress,
149+
): Promise<R> {
150+
const amt = amount.units(quote.params.normalizedParams.amount);
151+
const isEthereum = request.fromChain.chain === "Ethereum";
152+
const nativeTbtc = TBTCBridge.getNativeTbtcToken(request.fromChain.chain);
153+
const isNativeTbtc = nativeTbtc && isSameToken(quote.sourceToken.token, nativeTbtc);
154+
155+
if (isNativeTbtc && !isEthereum) {
156+
return await this.transferGateway(request, signer, to, amt);
157+
}
158+
159+
if (!isNativeTbtc && isEthereum) {
160+
throw new Error("Only tbtc can be transferred on Ethereum");
161+
}
162+
163+
if (!isNativeTbtc) {
164+
const tb = await request.fromChain.getTokenBridge();
165+
const originalAsset = await tb.getOriginalAsset(quote.sourceToken.token.address);
166+
const ethTbtc = TBTCBridge.getNativeTbtcToken("Ethereum")!;
167+
if (!isSameToken(originalAsset, ethTbtc)) {
168+
throw new Error("Can only transfer wrapped tbtc");
169+
}
170+
}
171+
172+
return await this.transferTokenBridge(request, signer, to, amt);
173+
}
174+
175+
private async transferGateway(
176+
request: RouteTransferRequest<N>,
177+
signer: Signer,
178+
to: ChainAddress,
179+
amt: bigint,
180+
): Promise<R> {
181+
const sender = Wormhole.parseAddress(signer.chain(), signer.address());
182+
const bridge = await request.fromChain.getTBTCBridge();
183+
const xfer = bridge.transfer(sender, to, amt);
184+
const txIds = await signSendWait(request.fromChain, xfer, signer);
185+
186+
const receipt: SourceInitiatedTransferReceipt = {
187+
originTxs: txIds,
188+
state: TransferState.SourceInitiated,
189+
from: request.fromChain.chain,
190+
to: request.toChain.chain,
191+
};
192+
193+
return receipt;
194+
}
195+
196+
private async transferTokenBridge(
197+
request: RouteTransferRequest<N>,
198+
signer: Signer,
199+
to: ChainAddress,
200+
amt: bigint,
201+
): Promise<R> {
202+
const sender = Wormhole.parseAddress(signer.chain(), signer.address());
203+
const toGateway = contracts.tbtc.get(request.fromChain.network, to.chain);
204+
const tb = await request.fromChain.getTokenBridge();
205+
let xfer;
206+
207+
if (toGateway) {
208+
// payload3 transfer to gateway contract
209+
xfer = tb.transfer(
210+
sender,
211+
Wormhole.chainAddress(request.toChain.chain, toGateway),
212+
request.source.id.address,
213+
amt,
214+
// payload is the recipient address
215+
to.address.toUniversalAddress().toUint8Array(),
216+
);
217+
} else {
218+
xfer = tb.transfer(sender, to, request.source.id.address, amt);
219+
}
220+
221+
const txIds = await signSendWait(request.fromChain, xfer, signer);
222+
223+
const receipt: SourceInitiatedTransferReceipt = {
224+
originTxs: txIds,
225+
state: TransferState.SourceInitiated,
226+
from: request.fromChain.chain,
227+
to: request.toChain.chain,
228+
};
229+
230+
return receipt;
231+
}
232+
233+
async complete(signer: Signer, receipt: R): Promise<R> {
234+
if (!isAttested(receipt)) {
235+
throw new Error("The source must be finalized in order to complete the transfer");
236+
}
237+
238+
const sender = Wormhole.parseAddress(signer.chain(), signer.address());
239+
const vaa = receipt.attestation.attestation;
240+
const toChain = this.wh.getChain(receipt.to);
241+
let xfer;
242+
243+
if (vaa.payloadLiteral === "TBTCBridge:GatewayTransfer") {
244+
const bridge = await toChain.getTBTCBridge();
245+
xfer = bridge.redeem(sender, vaa);
246+
} else {
247+
const tb = await toChain.getTokenBridge();
248+
// This is really a TokenBridge:Transfer VAA
249+
const serialized = serialize(vaa);
250+
const tbVaa = deserialize("TokenBridge:Transfer", serialized);
251+
xfer = tb.redeem(sender, tbVaa);
252+
}
253+
254+
const dstTxIds = await signSendWait(toChain, xfer, signer);
255+
256+
return {
257+
...receipt,
258+
state: TransferState.DestinationInitiated,
259+
destinationTxs: dstTxIds,
260+
};
261+
}
262+
263+
async resume(txid: TransactionId): Promise<R> {
264+
//const xfer = await TokenTransfer.from(this.wh, txid, 10 * 1000);
265+
//return TokenTransfer.getReceipt(xfer);
266+
throw new Error("Method not implemented.");
267+
}
268+
269+
async *track(receipt: Receipt, timeout?: number) {
270+
if (isSourceInitiated(receipt) || isSourceFinalized(receipt)) {
271+
const { txid } = receipt.originTxs[receipt.originTxs.length - 1]!;
272+
273+
const vaa = await this.wh.getVaa(txid, TBTCBridge.getTransferDiscriminator(), timeout);
274+
if (!vaa) throw new Error("No VAA found for transaction: " + txid);
275+
276+
const msgId: WormholeMessageId = {
277+
chain: vaa.emitterChain,
278+
emitter: vaa.emitterAddress,
279+
sequence: vaa.sequence,
280+
};
281+
282+
receipt = {
283+
...receipt,
284+
state: TransferState.Attested,
285+
attestation: {
286+
id: msgId,
287+
attestation: vaa,
288+
},
289+
} satisfies AttestedTransferReceipt<AttestationReceipt<"TBTCBridge">>;
290+
291+
yield receipt;
292+
}
293+
294+
if (isAttested(receipt)) {
295+
const toChain = this.wh.getChain(receipt.to);
296+
const toBridge = await toChain.getTokenBridge();
297+
const isCompleted = await toBridge.isTransferCompleted(receipt.attestation.attestation);
298+
if (isCompleted) {
299+
receipt = {
300+
...receipt,
301+
state: TransferState.DestinationFinalized,
302+
} satisfies CompletedTransferReceipt<AttestationReceipt<"TBTCBridge">>;
303+
304+
yield receipt;
305+
}
306+
}
307+
308+
yield receipt;
309+
}
310+
311+
static async isSourceTokenSupported<N extends Network>(
312+
sourceToken: TokenId,
313+
fromChain: ChainContext<N>,
314+
): Promise<boolean> {
315+
// Native tbtc is supported
316+
const nativeTbtc = TBTCBridge.getNativeTbtcToken(fromChain.chain);
317+
if (nativeTbtc && isSameToken(sourceToken, nativeTbtc)) {
318+
return true;
319+
}
320+
321+
// Wormhole-wrapped Ethereum tbtc is supported
322+
const tb = await fromChain.getTokenBridge();
323+
try {
324+
const originalAsset = await tb.getOriginalAsset(sourceToken.address);
325+
return isSameToken(originalAsset, TBTCBridge.getNativeTbtcToken("Ethereum")!);
326+
} catch (e: any) {
327+
if (e.message.includes("not a wrapped asset")) return false;
328+
throw e;
329+
}
330+
}
331+
}

core/base/src/constants/contracts/index.ts

+14-12
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
import * as core from './core.js';
2-
import * as tb from './tokenBridge.js';
3-
import * as tbr from './tokenBridgeRelayer.js';
4-
import * as nb from './nftBridge.js';
5-
import * as r from './relayer.js';
6-
import * as circle from './circle.js';
7-
import * as g from './cosmos.js';
8-
import * as rollup from './rollupCheckpoint.js';
9-
import * as p from './portico.js';
1+
import * as core from "./core.js";
2+
import * as tb from "./tokenBridge.js";
3+
import * as tbr from "./tokenBridgeRelayer.js";
4+
import * as nb from "./nftBridge.js";
5+
import * as r from "./relayer.js";
6+
import * as circle from "./circle.js";
7+
import * as g from "./cosmos.js";
8+
import * as rollup from "./rollupCheckpoint.js";
9+
import * as p from "./portico.js";
10+
import * as t from "./tbtc.js";
1011

11-
import { constMap } from './../../utils/index.js';
12+
import { constMap } from "./../../utils/index.js";
1213

1314
export const coreBridge = constMap(core.coreBridgeContracts);
1415
export const tokenBridge = constMap(tb.tokenBridgeContracts);
@@ -18,11 +19,12 @@ export const relayer = constMap(r.relayerContracts);
1819
export const gateway = constMap(g.gatewayContracts);
1920
export const translator = constMap(g.translatorContracts);
2021
export const portico = constMap(p.porticoContracts);
22+
export const tbtc = constMap(t.tbtcContracts);
2123

22-
export type { CircleContracts } from './circle.js';
24+
export type { CircleContracts } from "./circle.js";
2325
export const circleContracts = constMap(circle.circleContracts);
2426

25-
export type { PorticoContracts } from './portico.js';
27+
export type { PorticoContracts } from "./portico.js";
2628
export const rollupContracts = constMap(rollup.rollupContractAddresses);
2729

2830
// @ts-ignore: Adding one more token bridge is causing "Type instantiation is excessively deep and possibly infinite."

0 commit comments

Comments
 (0)