diff --git a/.changeset/few-rivers-fix.md b/.changeset/few-rivers-fix.md new file mode 100644 index 00000000..0f40c267 --- /dev/null +++ b/.changeset/few-rivers-fix.md @@ -0,0 +1,5 @@ +--- +'@fuel-bridge/test-utils': minor +--- + +Improve sway scripts diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 397235b0..9e60e4ee 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -14,8 +14,10 @@ "scripts": { "build": "tsup", "build:watch": "tsup --watch", - "deploy:bridge": "pnpm ts-node src/scripts/bridge.ts", - "deploy:relay": "pnpm ts-node src/scripts/relay-deposit.ts" + "bridge:deploy": "pnpm ts-node src/scripts/deploy-bridge.ts", + "bridge:upgrade": "pnpm ts-node src/scripts/upgrade-bridge.ts", + "bridge:transfer-ownership": "pnpm ts-node src/scripts/transfer-bridge-ownership.ts", + "bridge:relay": "pnpm ts-node src/scripts/relay-deposit.ts" }, "peerDependencies": { "fuels": "0.94.4", @@ -25,8 +27,10 @@ "@fuel-bridge/fungible-token": "workspace:*", "@fuel-bridge/message-predicates": "workspace:*", "@fuel-bridge/solidity-contracts": "workspace:*", + "@inquirer/prompts": "^5.3.8", "dotenv": "^16.0.3", - "typescript": "^5.1.6", - "ts-node": "^10.9.1" + "inquirer": "^10.1.8", + "ts-node": "^10.9.1", + "typescript": "^5.1.6" } } \ No newline at end of file diff --git a/packages/test-utils/src/scripts/bridge.ts b/packages/test-utils/src/scripts/deploy-bridge.ts similarity index 92% rename from packages/test-utils/src/scripts/bridge.ts rename to packages/test-utils/src/scripts/deploy-bridge.ts index 2d61a9c4..e7bb6e68 100644 --- a/packages/test-utils/src/scripts/bridge.ts +++ b/packages/test-utils/src/scripts/deploy-bridge.ts @@ -16,8 +16,9 @@ import { WalletUnlocked, ZeroBytes32, } from 'fuels'; +import { password } from '@inquirer/prompts'; -const { L1_TOKEN_GATEWAY, L2_SIGNER, L2_RPC } = process.env; +let { L1_TOKEN_GATEWAY, L2_SIGNER, L2_RPC } = process.env; // This helper avoids an exception in the case that the contract // was already deployed, and returns the contract instead @@ -37,6 +38,11 @@ function fetchIfDeployed(provider: Provider, wallet: WalletUnlocked) { const main = async () => { const provider = await Provider.create(L2_RPC, { resourceCacheTTL: -1 }); + + if (!L2_SIGNER) { + L2_SIGNER = await password({ message: 'Enter private key' }); + } + const wallet = Wallet.fromPrivateKey(L2_SIGNER, provider); console.log('\t> L2 Bridge deployment script initiated'); diff --git a/packages/test-utils/src/scripts/relay-deposit.ts b/packages/test-utils/src/scripts/relay-deposit.ts index d81952cc..7c8e4ac3 100644 --- a/packages/test-utils/src/scripts/relay-deposit.ts +++ b/packages/test-utils/src/scripts/relay-deposit.ts @@ -1,6 +1,6 @@ /** - * This is a stand-alone script that deploys the - * fetches a deposit messages and relays it to the bridge + * This is a stand-alone script that + * fetches a deposit message and relays it to the bridge */ import { Proxy } from '@fuel-bridge/fungible-token'; @@ -16,6 +16,7 @@ import { getPredicateRoot, hexlify, } from 'fuels'; +import { password } from '@inquirer/prompts'; import { FUEL_MESSAGE_TIMEOUT_MS, debug, @@ -25,11 +26,16 @@ import { const TOKEN_RECIPIENT_DATA_OFFSET = 160; -const { L2_SIGNER, L2_RPC, L2_BRIDGE_ID, L2_MESSAGE_NONCE, L2_TOKEN_RECEIVER } = +let { L2_SIGNER, L2_RPC, L2_BRIDGE_ID, L2_MESSAGE_NONCE, L2_TOKEN_RECEIVER } = process.env; const main = async () => { const provider = await Provider.create(L2_RPC, { resourceCacheTTL: -1 }); + + if (!L2_SIGNER) { + L2_SIGNER = await password({ message: 'Enter private key' }); + } + const wallet = Wallet.fromPrivateKey(L2_SIGNER, provider); const proxy = new Proxy(L2_BRIDGE_ID, wallet); @@ -43,47 +49,58 @@ const main = async () => { .proxy_target() .dryRun() .then((result) => { - debug('bridge_proxy.target() succeeded, assuming proxy'); + debug(`.proxy_target() returned ${result.value.bits}, assuming proxy`); return result.value.bits; }) .catch(() => { - debug('bridge.proxy_target() errored, assuming not proxy'); + debug('.proxy_target() errored, assuming not proxy'); return null; }); const predicateRoot = getPredicateRoot(contractMessagePredicate); let nonce: BN; + let endCursor: string | undefined; if (L2_MESSAGE_NONCE) nonce = new BN(L2_MESSAGE_NONCE); - else { - const response = await provider.getMessages(predicateRoot); - if (!response.messages || response.messages.length === 0) { - console.log('No messages in the predicate'); - return; + else + while (true) { + const response = await provider.getMessages(predicateRoot, { + after: endCursor, + }); + + if (!response.messages || response.messages.length === 0) { + console.log('No messages in the predicate'); + return; + } + + const { messages } = response; + + const message = messages.find((message) => { + const hex = hexlify(message.data).replace('0x', ''); + const recipient = hex.substring( + TOKEN_RECIPIENT_DATA_OFFSET * 2, + TOKEN_RECIPIENT_DATA_OFFSET * 2 + 64 // Recipient is 32 bytes + ); + const expectedRecipient = L2_TOKEN_RECEIVER || wallet.address.toB256(); + + return recipient === expectedRecipient.replace('0x', ''); + }); + + if (!message) { + if (response.pageInfo.hasNextPage) { + endCursor = response.pageInfo.endCursor; + continue; + } else { + console.log('No messages for the recipient'); + return; + } + } + + nonce = new BN(message.nonce); + break; } - const { messages } = response; - - const message = messages.find((message) => { - const hex = hexlify(message.data).replace('0x', ''); - const recipient = hex.substring( - TOKEN_RECIPIENT_DATA_OFFSET * 2, - TOKEN_RECIPIENT_DATA_OFFSET * 2 + 64 // Recipient is 32 bytes - ); - const expectedRecipient = L2_TOKEN_RECEIVER || wallet.address.toB256(); - - return recipient === expectedRecipient.replace('0x', ''); - }); - - if (!message) { - console.log('No messages for the recipient'); - return; - } - - nonce = new BN(message.nonce); - } - const message = await waitForMessage( provider, new Account(predicateRoot).address, @@ -105,6 +122,10 @@ const main = async () => { if (txResult.status === TransactionStatus.success) { console.log('\t> Transaction succeeded'); + console.log( + '\t > Minted asset IDs: ', + txResult.mintedAssets.map((asset) => asset.assetId) + ); } else { console.log('\t> Transaction errored'); } diff --git a/packages/test-utils/src/scripts/transfer-bridge-ownership.ts b/packages/test-utils/src/scripts/transfer-bridge-ownership.ts new file mode 100644 index 00000000..f958b547 --- /dev/null +++ b/packages/test-utils/src/scripts/transfer-bridge-ownership.ts @@ -0,0 +1,73 @@ +/** + * This is a stand-alone script that upgrades the bridge + */ + +import { Proxy } from '@fuel-bridge/fungible-token'; + +import { Provider, Wallet } from 'fuels'; +import { password } from '@inquirer/prompts'; +import { debug } from '../utils'; + +let { L2_SIGNER, L2_RPC, L2_BRIDGE_ID, L2_NEW_OWNER } = process.env; + +const main = async () => { + const provider = await Provider.create(L2_RPC, { resourceCacheTTL: -1 }); + + if (!L2_SIGNER) { + L2_SIGNER = await password({ message: 'Enter private key' }); + } + + const wallet = Wallet.fromPrivateKey(L2_SIGNER, provider); + + const proxy = new Proxy(L2_BRIDGE_ID, wallet); + + console.log('\t> L2 Bridge deployment script initiated'); + console.log('\t> Loaded wallet', wallet.address.toB256()); + console.log('\t> Balance: ', (await wallet.getBalance()).toString()); + + debug('Detecting if the bridge is a proxy...'); + let owner: string | null = await proxy.functions + ._proxy_owner() + .dryRun() + .then((result) => { + debug('bridge._proxy.owner() succeeded, assuming proxy'); + return result.value.Initialized.Address.bits; + }) + .catch((e) => { + debug(`bridge._proxy_owner() failed with error: `); + debug(`${JSON.stringify(e, undefined, 2)}`); + return null; + }); + + if (owner === null) { + console.log('Could not fetch the bridge owner, is it a proxy?'); + return; + } + + if ( + owner.replace('0x', '').toLowerCase() !== + wallet.address.toB256().replace('0x', '').toLowerCase() + ) { + console.log(`Owner mismatch, contract owned by ${owner}`); + return; + } + + const addressInput = { bits: L2_NEW_OWNER }; + const addressIdentityInput = { Address: addressInput }; + const tx = await proxy.functions + ._proxy_change_owner(addressIdentityInput) + .call(); + + console.log('\tTransaction ID: ', tx.transactionId); + await tx.waitForResult(); +}; + +main() + .then(() => { + console.log('\t> Finished'); + process.exit(0); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/packages/test-utils/src/scripts/upgrade-bridge.ts b/packages/test-utils/src/scripts/upgrade-bridge.ts new file mode 100644 index 00000000..1c528b37 --- /dev/null +++ b/packages/test-utils/src/scripts/upgrade-bridge.ts @@ -0,0 +1,145 @@ +/** + * This is a stand-alone script that upgrades the bridge + */ + +import { + Proxy, + BridgeFungibleTokenFactory, + BridgeFungibleToken, +} from '@fuel-bridge/fungible-token'; + +import { Provider, TransactionStatus, Wallet, ZeroBytes32 } from 'fuels'; +import { password } from '@inquirer/prompts'; +import { debug } from '../utils'; + +let { L1_TOKEN_GATEWAY, L2_SIGNER, L2_RPC, L2_BRIDGE_ID } = process.env; + +const main = async () => { + const provider = await Provider.create(L2_RPC!, { resourceCacheTTL: -1 }); + if (!L2_SIGNER) { + L2_SIGNER = await password({ message: 'Enter private key' }); + } + const wallet = Wallet.fromPrivateKey(L2_SIGNER, provider); + + const proxy = new Proxy(L2_BRIDGE_ID!, wallet); + + console.log('\t> L2 Bridge deployment script initiated'); + console.log('\t> Loaded wallet', wallet.address.toB256()); + console.log('\t> Balance: ', (await wallet.getBalance()).toString()); + + debug('Detecting if the bridge is a proxy: implementation'); + let current_implementation: string = await proxy.functions + .proxy_target() + .dryRun() + .then((result) => { + debug('bridge.proxy_target() returned, assuming proxy'); + if (!result.value.bits) { + return null; + } + return result.value.bits; + }) + .catch((e) => { + debug(`bridge.proxy_target() failed with error: `); + debug(`${JSON.stringify(e, undefined, 2)}`); + return null; + }); + debug(`Current implementation at ${current_implementation}`); + + debug('Detecting if the bridge is a proxy: owner'); + let owner: string | null = await proxy.functions + ._proxy_owner() + .dryRun() + .then((result) => { + debug('bridge._proxy.owner() succeeded, assuming proxy'); + if (!result.value.Initialized?.Address?.bits) { + return null; + } + return result?.value?.Initialized?.Address?.bits; + }) + .catch((e) => { + debug(`bridge._proxy_owner() failed with error: `); + debug(`${JSON.stringify(e, undefined, 2)}`); + return null; + }); + + if (owner === null) { + console.log('Could not fetch the bridge owner, is it a proxy?'); + return; + } + + if ( + owner.replace('0x', '').toLowerCase() !== + wallet.address.toB256().replace('0x', '').toLowerCase() + ) { + console.log(`Owner mismatch, contract owned by ${owner}`); + return; + } + + const implConfigurables: any = { + BRIDGED_TOKEN_GATEWAY: + '0x000000000000000000000000' + + L1_TOKEN_GATEWAY!.replace('0x', '').toLowerCase(), + }; + + const deployOpts = { + storageSlots: BridgeFungibleToken.storageSlots, + configurableConstants: implConfigurables, + salt: ZeroBytes32, + }; + + const factory = new BridgeFungibleTokenFactory(wallet); + factory.setConfigurableConstants(implConfigurables); + const { contractId } = factory.createTransactionRequest(deployOpts); + + if (contractId === current_implementation) { + console.log(`Implementation ${contractId} is already live in the proxy`); + return; + } + + const contractExists = (await provider.getContract(contractId)) !== null; + + if (!contractExists) { + debug('Deploying contract'); + const createTx = await factory.deployAsCreateTx(deployOpts); + debug('Expected contract ID', contractId); + debug('Fetching transaction ID'); + const createTxId = await createTx.waitForTransactionId(); + + debug(`Deploy transaction ${createTxId} sent, waiting for result`); + + const createTxResult = await createTx.waitForResult(); + if (createTxResult.transactionResult.status !== TransactionStatus.success) { + console.log('Could not deploy contract'); + debug(JSON.stringify(createTxResult, undefined, 2)); + return; + } + + if (createTx.contractId !== contractId) { + console.log('Contract mismatch, aborting'); + return; + } + + debug('Contract deployment completed'); + debug('Deploy opts', deployOpts); + } + + console.log('New implementation at ', contractId); + + const contractIdentityInput = { bits: contractId }; + const tx = await proxy.functions + .set_proxy_target(contractIdentityInput) + .call(); + + console.log('\tTransaction ID: ', tx.transactionId); + await tx.waitForResult(); +}; + +main() + .then(() => { + console.log('\t> Finished'); + process.exit(0); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/packages/test-utils/src/utils/fuels/relayCommonMessage.ts b/packages/test-utils/src/utils/fuels/relayCommonMessage.ts index 0ff7daa4..d48c87d2 100644 --- a/packages/test-utils/src/utils/fuels/relayCommonMessage.ts +++ b/packages/test-utils/src/utils/fuels/relayCommonMessage.ts @@ -48,6 +48,9 @@ type CommonMessageDetails = { ) => Promise; }; +// Update for mainnet gas costs +const PREDICATE_GAS_LIMIT = 10000000; + // Details for relaying common messages with certain predicate roots function getCommonRelayableMessages(provider: Provider) { // Create a predicate for common messages @@ -150,7 +153,7 @@ function getCommonRelayableMessages(provider: Provider) { }); transaction.witnesses.push(ZeroBytes32); - transaction.gasLimit = bn(500_000); + transaction.gasLimit = bn(PREDICATE_GAS_LIMIT); debug( '-------------------------------------------------------------------' @@ -229,7 +232,6 @@ export async function relayCommonMessage( } estimated_tx.maxFee = fees.maxFee; - estimated_tx.gasLimit = fees.gasLimit.mul(4).div(3); const simulation = await relayer.simulateTransaction(estimated_tx); debug(simulation); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c220228..deaae569 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -272,9 +272,15 @@ importers: '@fuel-bridge/solidity-contracts': specifier: workspace:* version: link:../solidity-contracts + '@inquirer/prompts': + specifier: ^5.3.8 + version: 5.3.8 dotenv: specifier: ^16.0.3 version: 16.4.5 + inquirer: + specifier: ^10.1.8 + version: 10.1.8 ts-node: specifier: ^10.9.1 version: 10.9.2(@types/node@22.5.0)(typescript@5.4.5) @@ -1117,6 +1123,7 @@ packages: '@humanwhocodes/config-array@0.11.14': resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} @@ -1124,6 +1131,7 @@ packages: '@humanwhocodes/object-schema@2.0.3': resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead '@inquirer/checkbox@2.4.7': resolution: {integrity: sha512-5YwCySyV1UEgqzz34gNsC38eKxRBtlRDpJLlKcRtTjlYA/yDKuc1rfw+hjw+2WJxbAZtaDPsRl5Zk7J14SBoBw==} @@ -1426,9 +1434,11 @@ packages: '@openzeppelin/defender-admin-client@1.54.1': resolution: {integrity: sha512-kRpSUdTsnSqntp4FOXIm95t+6VKHc8CUY2Si71VDuxs0q7HSPZkdpRPSntcolwEzWy9L4a8NS/QMwDF5NJ4X1g==} + deprecated: This package has been deprecated and will no longer be maintained, please use @openzeppelin/defender-sdk package instead. '@openzeppelin/defender-base-client@1.54.1': resolution: {integrity: sha512-DRGz/7KN3ZQwu28YWMOaojrC7jjPkz/uCwkC8/C8B11qwZhA5qIVvyhYHhhFOCl0J84+E3TNdvkPD2q3p2WaJw==} + deprecated: This package has been deprecated and will no longer be maintained, please use @openzeppelin/defender-sdk package instead. '@openzeppelin/defender-sdk-base-client@1.12.0': resolution: {integrity: sha512-6n5SwJWCjFciC+T7QhfXnkEmkNZGKK0efkCZDAfu/Kd8L1AICnIkWwVte71dnzCBln7wglMiZupSHL3fZqePmQ==} @@ -2082,9 +2092,6 @@ packages: resolution: {integrity: sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==} engines: {node: '>=4'} - axios@1.6.8: - resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==} - axios@1.7.5: resolution: {integrity: sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==} @@ -2860,6 +2867,7 @@ packages: ethereum-bloom-filters@1.1.0: resolution: {integrity: sha512-J1gDRkLpuGNvWYzWslBQR9cDV4nd4kfvVTE/Wy4Kkm4yb3EYRSlyi0eB/inTsSTTVyA0+HyzHgbr95Fn/Z1fSw==} + deprecated: do not use this package use package versions above as this can miss some topics ethereum-cryptography@0.1.3: resolution: {integrity: sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ==} @@ -3136,23 +3144,29 @@ packages: glob@5.0.15: resolution: {integrity: sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==} + deprecated: Glob versions prior to v9 are no longer supported glob@7.1.7: resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} + deprecated: Glob versions prior to v9 are no longer supported glob@7.2.0: resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + deprecated: Glob versions prior to v9 are no longer supported glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported glob@8.0.3: resolution: {integrity: sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==} engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported global-modules@2.0.0: resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} @@ -3346,6 +3360,7 @@ packages: inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -4401,10 +4416,12 @@ packages: rimraf@2.6.3: resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rimraf@2.7.1: resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rimraf@3.0.2: @@ -7565,14 +7582,6 @@ snapshots: axe-core@4.7.0: {} - axios@1.6.8(debug@4.3.4): - dependencies: - follow-redirects: 1.15.6(debug@4.3.4) - form-data: 4.0.0 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - axios@1.7.5(debug@4.3.4): dependencies: follow-redirects: 1.15.6(debug@4.3.4) @@ -9218,7 +9227,7 @@ snapshots: '@ethersproject/transactions': 5.7.0 '@ethersproject/wallet': 5.7.0 '@types/qs': 6.9.15 - axios: 1.6.8(debug@4.3.4) + axios: 1.7.5(debug@4.3.4) chalk: 4.1.2 chokidar: 3.6.0 debug: 4.3.4(supports-color@8.1.1)