11#!/usr/bin/env bunx tsx
22
3+ import { randomBytes } from 'crypto'
4+
35import { consola } from 'consola'
46import { config } from 'dotenv'
5- import { BigNumber , constants , utils } from 'ethers'
7+ import {
8+ encodeFunctionData ,
9+ formatUnits ,
10+ getAddress ,
11+ getContract ,
12+ parseUnits ,
13+ toHex ,
14+ zeroAddress ,
15+ } from 'viem'
616
717import {
18+ ERC20__factory ,
19+ PolymerCCTPFacet__factory ,
820 type ILiFi ,
921 type PolymerCCTPFacet ,
10- PolymerCCTPFacet__factory ,
1122} from '../../typechain'
1223
1324import {
@@ -16,11 +27,13 @@ import {
1627 ADDRESS_USDC_OPT ,
1728 ADDRESS_USDC_SOL ,
1829 DEV_WALLET_ADDRESS ,
19- ensureBalanceAndAllowanceToDiamond ,
20- getProvider ,
21- getWalletFromPrivateKeyInDotEnv ,
30+ NON_EVM_ADDRESS ,
31+ createContractObject ,
32+ ensureAllowance ,
33+ ensureBalance ,
34+ executeTransaction ,
2235 logBridgeDataStruct ,
23- sendTransaction ,
36+ setupEnvironment ,
2437} from './utils/demoScriptHelpers'
2538
2639config ( )
@@ -44,37 +57,40 @@ const SRC_CHAIN = 'optimism' as 'arbitrum' | 'optimism'
4457// these are test deployments by Polymer team
4558const LIFI_DIAMOND_ADDRESS_ARB = '0xD99A49304227d3fE2c27A1F12Ef66A95b95837b6'
4659const LIFI_DIAMOND_ADDRESS_OPT = '0x36d7A6e0B2FE968a9558C5AaF5713aC2DAc0DbFc'
47- const DIAMOND_ADDRESS_SRC =
60+ const DIAMOND_ADDRESS_SRC = getAddress (
4861 SRC_CHAIN === 'arbitrum' ? LIFI_DIAMOND_ADDRESS_ARB : LIFI_DIAMOND_ADDRESS_OPT
62+ )
4963
5064// Destination chain ID
5165const LIFI_CHAIN_ID_SOLANA = 1151111081099710
5266// Mainnet chain IDs
5367const LIFI_CHAIN_ID_ARBITRUM = 42161
5468const LIFI_CHAIN_ID_OPTIMISM = 10
55- const NON_EVM_ADDRESS = '0x11f111f111f111F111f111f111F111f111f111F1'
5669
5770// For EVM destinations, use Arbitrum if source is Optimism, and vice versa
5871const DST_CHAIN_ID_EVM =
5972 SRC_CHAIN === 'arbitrum' ? LIFI_CHAIN_ID_OPTIMISM : LIFI_CHAIN_ID_ARBITRUM // Optimism or Arbitrum
6073const DST_CHAIN_ID = BRIDGE_TO_SOLANA ? LIFI_CHAIN_ID_SOLANA : DST_CHAIN_ID_EVM
6174
6275// Token addresses
63- const sendingAssetId =
76+ const sendingAssetId = getAddress (
6477 SRC_CHAIN === 'arbitrum' ? ADDRESS_USDC_ARB : ADDRESS_USDC_OPT
65- const fromAmount = '1000000' // 1 USDC (6 decimals)
78+ )
79+ const fromAmount = parseUnits ( '1' , 6 ) // 1 USDC (6 decimals)
6680
6781// Receiver address
68- const receiverAddress = BRIDGE_TO_SOLANA
69- ? NON_EVM_ADDRESS // Use NON_EVM_ADDRESS for Solana
70- : DEV_WALLET_ADDRESS // Use EVM address for EVM destinations
82+ const receiverAddress = getAddress (
83+ BRIDGE_TO_SOLANA
84+ ? NON_EVM_ADDRESS // Use NON_EVM_ADDRESS for Solana
85+ : DEV_WALLET_ADDRESS
86+ ) // Use EVM address for EVM destinations
7187
7288// Solana receiver (bytes32 format) - only used when BRIDGE_TO_SOLANA is true
7389const solanaReceiverBytes32 = ADDRESS_DEV_WALLET_SOLANA_BYTES32
7490
7591// Polymer CCTP specific parameters
7692// polymerTokenFee will be extracted from API response
77- const maxCCTPFee = '0' // Max CCTP fee (0 = no limit)
93+ const maxCCTPFee = 0n // Max CCTP fee (0 = no limit)
7894const minFinalityThreshold = USE_FAST_MODE ? 1000 : 2000 // 1000 = fast path, 2000 = standard path
7995
8096const EXPLORER_BASE_URL =
@@ -129,9 +145,9 @@ async function getPolymerQuote(
129145 toChainId : number ,
130146 fromToken : string ,
131147 toToken : string ,
132- fromAmount : string ,
148+ fromAmount : bigint ,
133149 toAddress : string
134- ) : Promise < { quote : IQuoteResponse ; polymerTokenFee : string } > {
150+ ) : Promise < { quote : IQuoteResponse ; polymerTokenFee : bigint } > {
135151 const quoteType = USE_FAST_MODE ? 'fast' : 'standard'
136152 consola . info ( `\n📡 Fetching ${ quoteType } quote from Polymer API...` )
137153
@@ -141,14 +157,14 @@ async function getPolymerQuote(
141157 toChain : toChainId . toString ( ) ,
142158 fromToken,
143159 toToken,
144- fromAmount,
160+ fromAmount : fromAmount . toString ( ) ,
145161 toAddress,
146162 } )
147163
148164 const fullApiUrl = `${ POLYMER_API_URL } /v1/quote/${ quoteType } ?${ queryParams . toString ( ) } `
149165 consola . info ( `API URL: ${ fullApiUrl } ` )
150166 consola . info (
151- `Request: fromChain=${ fromChainId } , toChain=${ toChainId } , fromAmount=${ fromAmount } `
167+ `Request: fromChain=${ fromChainId } , toChain=${ toChainId } , fromAmount=${ fromAmount . toString ( ) } `
152168 )
153169
154170 const quoteResponse = await fetch ( fullApiUrl , {
@@ -175,7 +191,7 @@ async function getPolymerQuote(
175191 // Standard path: polymerTokenFee = 0 (no CCTP service fee)
176192 // Fast path: polymerTokenFee > 0 (CCTP service fee charged by Circle, included in amount)
177193 // The "CCTP service fee" with included: true is the fee deducted from the bridged amount
178- let polymerTokenFee = '0'
194+ let polymerTokenFee = 0n
179195 if ( quoteData . feeCosts ) {
180196 // Look for CCTP service fee (fast path) - this is the fee that's included/deducted
181197 const cctpServiceFee = quoteData . feeCosts . find (
@@ -185,7 +201,7 @@ async function getPolymerQuote(
185201 fee . included === true
186202 )
187203 if ( cctpServiceFee && cctpServiceFee . amount ) {
188- polymerTokenFee = cctpServiceFee . amount
204+ polymerTokenFee = BigInt ( cctpServiceFee . amount )
189205 }
190206 }
191207
@@ -199,17 +215,19 @@ async function getPolymerQuote(
199215}
200216
201217async function main ( ) {
202- const provider = getProvider ( SRC_CHAIN )
203- const wallet = getWalletFromPrivateKeyInDotEnv ( provider )
204- const walletAddress = await wallet . getAddress ( )
218+ // Setup environment using helper function
219+ const { client, publicClient, walletAccount, walletClient } =
220+ await setupEnvironment ( SRC_CHAIN , null )
221+ const walletAddress = walletAccount . address
205222 consola . info ( 'Using wallet address:' , walletAddress )
206223
207- // Get diamond contract
208- const polymerCCTPFacet = PolymerCCTPFacet__factory . connect (
209- DIAMOND_ADDRESS_SRC ,
210- wallet
211- )
212- consola . info ( 'Diamond/PolymerCCTPFacet connected:' , polymerCCTPFacet . address )
224+ // Get diamond contract with custom address
225+ const polymerCCTPFacet = getContract ( {
226+ address : DIAMOND_ADDRESS_SRC ,
227+ abi : PolymerCCTPFacet__factory . abi ,
228+ client,
229+ } )
230+ consola . info ( 'Diamond/PolymerCCTPFacet connected:' , DIAMOND_ADDRESS_SRC )
213231
214232 // Display route details
215233 consola . info ( '\n🌉 BRIDGE ROUTE DETAILS:' )
@@ -227,9 +245,7 @@ async function main() {
227245 consola . info (
228246 `📥 Destination Chain: ${ destChainName } (Chain ID: ${ DST_CHAIN_ID } )`
229247 )
230- consola . info (
231- `💰 Amount: ${ BigNumber . from ( fromAmount ) . div ( 1e6 ) . toString ( ) } USDC`
232- )
248+ consola . info ( `💰 Amount: ${ formatUnits ( fromAmount , 6 ) } USDC` )
233249 consola . info ( `🎯 Sending Asset: ${ sendingAssetId } ` )
234250 consola . info (
235251 `👤 Receiver: ${
@@ -275,15 +291,15 @@ async function main() {
275291 // Note: The facet transfers minAmount from user, then deducts polymerTokenFee before bridging
276292 // So if minAmount = fromAmount, the bridged amount will be fromAmount - polymerTokenFee
277293 // User must approve minAmount (which equals fromAmount in this case)
278- const transactionId = utils . randomBytes ( 32 )
294+ const transactionId = toHex ( new Uint8Array ( randomBytes ( 32 ) ) )
279295 const bridgeData : ILiFi . BridgeDataStruct = {
280296 transactionId,
281297 bridge : 'polymercctp' ,
282298 integrator : 'demoScript' ,
283- referrer : constants . AddressZero ,
299+ referrer : zeroAddress ,
284300 sendingAssetId,
285301 receiver : receiverAddress ,
286- minAmount : fromAmount , // Total amount to transfer; bridged amount = minAmount - polymerTokenFee
302+ minAmount : fromAmount . toString ( ) , // Total amount to transfer; bridged amount = minAmount - polymerTokenFee
287303 destinationChainId : DST_CHAIN_ID ,
288304 hasSourceSwaps : false ,
289305 hasDestinationCall : false ,
@@ -293,11 +309,11 @@ async function main() {
293309
294310 // Prepare PolymerCCTP data
295311 const polymerData : PolymerCCTPFacet . PolymerCCTPDataStruct = {
296- polymerTokenFee,
297- maxCCTPFee,
312+ polymerTokenFee : polymerTokenFee . toString ( ) ,
313+ maxCCTPFee : maxCCTPFee . toString ( ) ,
298314 nonEVMReceiver : BRIDGE_TO_SOLANA
299- ? solanaReceiverBytes32
300- : constants . HashZero ,
315+ ? toHex ( solanaReceiverBytes32 )
316+ : toHex ( 0 , { size : 32 } ) ,
301317 minFinalityThreshold,
302318 }
303319 consola . info ( '📋 polymerData prepared:' )
@@ -308,54 +324,54 @@ async function main() {
308324
309325 // Ensure balance and allowance
310326 // Contract transfers minAmount, so user must approve minAmount (fromAmount)
311- const totalAmountNeeded = BigNumber . from ( fromAmount )
312327 if ( SEND_TX ) {
313- await ensureBalanceAndAllowanceToDiamond (
328+ // Create ERC20 contract for balance/allowance checks
329+ const tokenContract = createContractObject (
314330 sendingAssetId ,
315- wallet ,
331+ ERC20__factory . abi ,
332+ publicClient ,
333+ walletClient
334+ )
335+
336+ await ensureBalance ( tokenContract , walletAddress , fromAmount , publicClient )
337+ await ensureAllowance (
338+ tokenContract ,
339+ walletAddress ,
316340 DIAMOND_ADDRESS_SRC ,
317- totalAmountNeeded ,
318- false
341+ fromAmount ,
342+ publicClient
319343 )
320344 consola . info ( '✅ Balance and allowance verified' )
321345 consola . info (
322- ` Approved amount: ${ totalAmountNeeded . toString ( ) } (will bridge: ${ BigNumber . from (
323- fromAmount
324- )
325- . sub ( BigNumber . from ( polymerTokenFee ) )
326- . toString ( ) } , fee: ${ polymerTokenFee } )`
346+ ` Approved amount: ${ fromAmount . toString ( ) } (will bridge: ${ (
347+ fromAmount - polymerTokenFee
348+ ) . toString ( ) } , fee: ${ polymerTokenFee . toString ( ) } )`
327349 )
328350 }
329351
330352 // Execute transaction
331353 if ( SEND_TX ) {
332354 consola . info ( '🚀 Executing bridge transaction...' )
333355
334- const executeTxData = await polymerCCTPFacet . populateTransaction
335- . startBridgeTokensViaPolymerCCTP ( bridgeData , polymerData )
336- . then ( ( tx ) => tx . data )
356+ const hash = await executeTransaction (
357+ ( ) =>
358+ ( polymerCCTPFacet . write as any ) . startBridgeTokensViaPolymerCCTP ( [
359+ bridgeData ,
360+ polymerData ,
361+ ] ) ,
362+ 'Starting bridge via PolymerCCTP' ,
363+ publicClient ,
364+ true
365+ )
337366
338- if ( ! executeTxData ) {
339- throw new Error ( 'Failed to populate transaction data ' )
367+ if ( ! hash ) {
368+ throw new Error ( 'Failed to execute transaction' )
340369 }
341370
342- const transactionResponse = await sendTransaction (
343- wallet ,
344- polymerCCTPFacet . address ,
345- executeTxData ,
346- BigNumber . from ( 0 )
347- )
348-
349371 consola . info ( '\n🎉 BRIDGE TRANSACTION EXECUTED SUCCESSFULLY!' )
350- consola . info ( `📤 Transaction Hash: ${ transactionResponse . hash } ` )
351- consola . info (
352- `🔗 Explorer Link: ${ EXPLORER_BASE_URL } ${ transactionResponse . hash } `
353- )
354- consola . info (
355- `💰 Amount Bridged: ${ BigNumber . from ( fromAmount )
356- . div ( 1e6 )
357- . toString ( ) } USDC`
358- )
372+ consola . info ( `📤 Transaction Hash: ${ hash } ` )
373+ consola . info ( `🔗 Explorer Link: ${ EXPLORER_BASE_URL } ${ hash } ` )
374+ consola . info ( `💰 Amount Bridged: ${ formatUnits ( fromAmount , 6 ) } USDC` )
359375 consola . info (
360376 `📥 Destination: ${
361377 BRIDGE_TO_SOLANA
@@ -368,9 +384,11 @@ async function main() {
368384 consola . info ( '' )
369385 } else {
370386 consola . info ( '🔍 Dry-run mode: Transaction not sent' )
371- const executeTxData = await polymerCCTPFacet . populateTransaction
372- . startBridgeTokensViaPolymerCCTP ( bridgeData , polymerData )
373- . then ( ( tx ) => tx . data )
387+ const executeTxData = encodeFunctionData ( {
388+ abi : PolymerCCTPFacet__factory . abi ,
389+ functionName : 'startBridgeTokensViaPolymerCCTP' ,
390+ args : [ bridgeData , polymerData ] as any ,
391+ } )
374392 consola . info ( 'Calldata:' , executeTxData )
375393 }
376394}
0 commit comments