diff --git a/.gitmodules b/.gitmodules index be45593e9d..661671bb01 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,10 +2,3 @@ path = l2geth/tests/testdata url = https://github.com/ethereum/tests -[submodule "bedrock/erigon"] - path = bedrock/erigon - url = https://github.com/bobanetwork/erigon - -[submodule "bedrock/reference-optimistic-geth"] - path = bedrock/reference-optimistic-geth - url = https://github.com/bobanetwork/reference-optimistic-geth diff --git a/bedrock/erigon b/bedrock/erigon deleted file mode 160000 index 587af30238..0000000000 --- a/bedrock/erigon +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 587af302385d9ed8cf5a4d079ed31e996bec009e diff --git a/bedrock/reference-optimistic-geth b/bedrock/reference-optimistic-geth deleted file mode 160000 index b888babf44..0000000000 --- a/bedrock/reference-optimistic-geth +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b888babf44e5bdf0cb79ba6a9fc45ed3f7045933 diff --git a/integration-tests/contracts/TestFailingMintL1StandardERC721.sol b/integration-tests/contracts/TestFailingMintL1StandardERC721.sol new file mode 100644 index 0000000000..f7208ec34a --- /dev/null +++ b/integration-tests/contracts/TestFailingMintL1StandardERC721.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.7.5; + +import "@boba/contracts/contracts/standards/L1StandardERC721.sol"; + +/** +* A Failing mint L1ERC721 contract +*/ +contract TestFailingMintL1StandardERC721 is L1StandardERC721 { + /** + * @param _l1Bridge Address of the L1 standard bridge. + * @param _l2Contract Address of the corresponding L2 NFT contract. + * @param _name ERC721 name. + * @param _symbol ERC721 symbol. + */ + constructor( + address _l1Bridge, + address _l2Contract, + string memory _name, + string memory _symbol, + string memory _baseTokenURI + ) + L1StandardERC721(_l1Bridge, _l2Contract, _name, _symbol, _baseTokenURI) {} + + function mint(address _to, uint256 _tokenId, bytes memory _data) public virtual override onlyL1Bridge { + revert("mint failing"); + } +} diff --git a/integration-tests/contracts/TestFailingMintL2StandardERC721.sol b/integration-tests/contracts/TestFailingMintL2StandardERC721.sol new file mode 100644 index 0000000000..44c6998861 --- /dev/null +++ b/integration-tests/contracts/TestFailingMintL2StandardERC721.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.7.5; + +import "@boba/contracts/contracts/standards/L2StandardERC721.sol"; + +/** +* A Failing mint L2ERC721 contract +*/ +contract TestFailingMintL2StandardERC721 is L2StandardERC721 { + /** + * @param _l2Bridge Address of the L2 standard bridge. + * @param _l1Contract Address of the corresponding L1 NFT contract. + * @param _name ERC721 name. + * @param _symbol ERC721 symbol. + */ + constructor( + address _l2Bridge, + address _l1Contract, + string memory _name, + string memory _symbol, + string memory _baseTokenURI + ) + L2StandardERC721(_l2Bridge, _l1Contract, _name, _symbol, _baseTokenURI) {} + + function mint(address _to, uint256 _tokenId, bytes memory _data) public virtual override onlyL2Bridge { + revert("mint failing"); + } +} diff --git a/integration-tests/test/nft_bridge.spec.ts b/integration-tests/test/nft_bridge.spec.ts index 150ea0d04c..bb78dddbc8 100644 --- a/integration-tests/test/nft_bridge.spec.ts +++ b/integration-tests/test/nft_bridge.spec.ts @@ -20,6 +20,9 @@ import L2ERC721ExtraDataJson from '../artifacts/contracts/TestExtraDataL2Standar import L2BillingContractJson from '@boba/contracts/artifacts/contracts/L2BillingContract.sol/L2BillingContract.json' import L2GovernanceERC20Json from '@boba/contracts/artifacts/contracts/standards/L2GovernanceERC20.sol/L2GovernanceERC20.json' +import L1ERC721FailingMintJson from '../artifacts/contracts/TestFailingMintL1StandardERC721.sol/TestFailingMintL1StandardERC721.json' +import L2ERC721FailingMintJson from '../artifacts/contracts/TestFailingMintL2StandardERC721.sol/TestFailingMintL2StandardERC721.json' + import { OptimismEnv } from './shared/env' import { ethers } from 'hardhat' @@ -2076,7 +2079,7 @@ describe('NFT Bridge Test', async () => { }) it('{tag:boba} should deposit NFT back without sending data for non-native token', async () => { - const approveTX = await L1ERC721.connect(env.l2Wallet).approve( + const approveTX = await L1ERC721.connect(env.l1Wallet).approve( L1Bridge.address, DUMMY_TOKEN_ID ) @@ -2275,6 +2278,170 @@ describe('NFT Bridge Test', async () => { }) }) + describe('L1 native NFT - failing mint on L2', async () => { + before(async () => { + Factory__L1ERC721 = new ContractFactory( + ERC721Json.abi, + ERC721Json.bytecode, + env.l1Wallet + ) + + Factory__L2ERC721 = new ContractFactory( + L2ERC721FailingMintJson.abi, + L2ERC721FailingMintJson.bytecode, + env.l2Wallet + ) + + L1ERC721 = await Factory__L1ERC721.deploy('Test', 'TST') + + await L1ERC721.deployTransaction.wait() + + L2ERC721 = await Factory__L2ERC721.deploy( + L2Bridge.address, + L1ERC721.address, + 'Test', + 'TST', + '' // base-uri + ) + + await L2ERC721.deployTransaction.wait() + + // register NFT + const registerL1BridgeTx = await L1Bridge.registerNFTPair( + L1ERC721.address, + L2ERC721.address, + 'L1' + ) + await registerL1BridgeTx.wait() + + const registerL2BridgeTx = await L2Bridge.registerNFTPair( + L1ERC721.address, + L2ERC721.address, + 'L1' + ) + await registerL2BridgeTx.wait() + }) + + it('{tag:boba} should try deposit NFT to L2', async () => { + // mint nft + const mintTx = await L1ERC721.mint(env.l1Wallet.address, DUMMY_TOKEN_ID) + await mintTx.wait() + + const approveTx = await L1ERC721.approve(L1Bridge.address, DUMMY_TOKEN_ID) + await approveTx.wait() + + const depositTx = await env.waitForXDomainTransaction( + await L1Bridge.depositNFT(L1ERC721.address, DUMMY_TOKEN_ID, 9999999) + ) + + // submit a random l2 tx, so the relayer is unstuck for the tests + await env.l2Wallet_2.sendTransaction({ + to: env.l2Wallet_2.address, + value: utils.parseEther('0.01'), + gasLimit: 1000000, + }) + + const backTx = await env.messenger.l2Provider.getTransaction( + depositTx.remoteReceipt.transactionHash + ) + await env.waitForXDomainTransaction(backTx) + + // check event DepositFailed is emittted + const returnedlogIndex = await getFilteredLogIndex( + depositTx.remoteReceipt, + L2NFTBridge.abi, + L2Bridge.address, + 'DepositFailed' + ) + const ifaceL2NFTBridge = new ethers.utils.Interface(L2NFTBridge.abi) + const log = ifaceL2NFTBridge.parseLog( + depositTx.remoteReceipt.logs[returnedlogIndex] + ) + expect(log.args._tokenId).to.deep.eq(DUMMY_TOKEN_ID) + + const ownerL1 = await L1ERC721.ownerOf(DUMMY_TOKEN_ID) + await expect(L2ERC721.ownerOf(DUMMY_TOKEN_ID)).to.be.revertedWith( + 'ERC721: owner query for nonexistent token' + ) + + expect(ownerL1).to.deep.eq(env.l1Wallet.address) + }).timeout(100000) + }) + + describe('L2 native NFT - failing mint on L1', async () => { + before(async () => { + Factory__L2ERC721 = new ContractFactory( + ERC721Json.abi, + ERC721Json.bytecode, + env.l2Wallet + ) + + Factory__L1ERC721 = new ContractFactory( + L1ERC721FailingMintJson.abi, + L1ERC721FailingMintJson.bytecode, + env.l1Wallet + ) + + // deploy a L2 native NFT token each time if existing contracts are used for tests + L2ERC721 = await Factory__L2ERC721.deploy('Test', 'TST') + + await L2ERC721.deployTransaction.wait() + + L1ERC721 = await Factory__L1ERC721.deploy( + L1Bridge.address, + L2ERC721.address, + 'Test', + 'TST', + '' // base-uri + ) + + await L1ERC721.deployTransaction.wait() + + // register NFT + const registerL1BridgeTx = await L1Bridge.registerNFTPair( + L1ERC721.address, + L2ERC721.address, + 'L2' + ) + await registerL1BridgeTx.wait() + + const registerL2BridgeTx = await L2Bridge.registerNFTPair( + L1ERC721.address, + L2ERC721.address, + 'L2' + ) + await registerL2BridgeTx.wait() + }) + + it('{tag:boba} should try exit NFT from L2', async () => { + // mint nft + const mintTx = await L2ERC721.mint(env.l2Wallet.address, DUMMY_TOKEN_ID) + await mintTx.wait() + + const approveTx = await L2ERC721.approve(L2Bridge.address, DUMMY_TOKEN_ID) + await approveTx.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + + await env.waitForRevertXDomainTransactionL1( + L2Bridge.withdraw(L2ERC721.address, DUMMY_TOKEN_ID, 9999999) + ) + + await expect(L1ERC721.ownerOf(DUMMY_TOKEN_ID)).to.be.revertedWith( + 'ERC721: owner query for nonexistent token' + ) + const ownerL2 = await L2ERC721.ownerOf(DUMMY_TOKEN_ID) + + expect(ownerL2).to.deep.eq(env.l2Wallet.address) + }).timeout(100000) + }) + describe('Bridges pause tests', async () => { before(async () => { Factory__L1ERC721 = new ContractFactory( diff --git a/integration-tests/test/shared/env.ts b/integration-tests/test/shared/env.ts index 19009cb4bf..d3a3252f70 100644 --- a/integration-tests/test/shared/env.ts +++ b/integration-tests/test/shared/env.ts @@ -471,15 +471,21 @@ export class OptimismEnv { } } - // async waitForRevertXDomainTransaction( - // tx: Promise | TransactionResponse - // ) { - // const { remoteReceipt } = await this.waitForXDomainTransaction(tx) - // const [xDomainMsgHash] = await this.messenger.getMessageHashesFromL2Tx( - // remoteReceipt.transactionHash - // ) - // await this.messenger.getL1TransactionReceipt(xDomainMsgHash) - // } + async waitForRevertXDomainTransactionL2( + tx: Promise | TransactionResponse + ) { + const { remoteReceipt } = await this.waitForXDomainTransaction(tx) + const backTx = await this.messenger.l2Provider.getTransaction(remoteReceipt.transactionHash) + await this.waitForXDomainTransaction(backTx) + } + + async waitForRevertXDomainTransactionL1( + tx: Promise | TransactionResponse + ) { + const { remoteReceipt } = await this.waitForXDomainTransaction(tx) + const backTx = await this.messenger.l1Provider.getTransaction(remoteReceipt.transactionHash) + await this.waitForXDomainTransaction(backTx) + } // async waitForRevertXDomainTransactionFast( // tx: Promise | TransactionResponse diff --git a/l2geth/rollup/rcfg/system_address.go b/l2geth/rollup/rcfg/system_address.go new file mode 100644 index 0000000000..e82b68e469 --- /dev/null +++ b/l2geth/rollup/rcfg/system_address.go @@ -0,0 +1,98 @@ +package rcfg + +import ( + "math/big" + "os" + + "github.com/ethereum-optimism/optimism/l2geth/common" +) + +// SystemAddress0 is the first deployable system address. +var SystemAddress0 = common.HexToAddress("0x4200000000000000000000000000000000000042") + +// SystemAddress1 is the second deployable system address. +var SystemAddress1 = common.HexToAddress("0x4200000000000000000000000000000000000014") + +// ZeroSystemAddress is the emprt system address. +var ZeroSystemAddress common.Address + +// SystemAddressDeployer is a tuple containing the deployment +// addresses for SystemAddress0 and SystemAddress1. +type SystemAddressDeployer [2]common.Address + +// SystemAddressFor returns the system address for a given deployment +// address. If no system address is configured for this deployer, +// ZeroSystemAddress is returned. +func (s SystemAddressDeployer) SystemAddressFor(addr common.Address) common.Address { + if s[0] == addr { + return SystemAddress0 + } + + if s[1] == addr { + return SystemAddress1 + } + + return ZeroSystemAddress +} + +// SystemAddressFor is a convenience method that returns an environment-based +// system address if the passed-in chain ID is not hardcoded. +func SystemAddressFor(chainID *big.Int, addr common.Address) common.Address { + sysDeployer, hasHardcodedSysDeployer := SystemAddressDeployers[chainID.Uint64()] + if !hasHardcodedSysDeployer { + sysDeployer = envSystemAddressDeployer + } + + return sysDeployer.SystemAddressFor(addr) +} + +// SystemAddressDeployers maintains a hardcoded map of chain IDs to +// system addresses. +var SystemAddressDeployers = map[uint64]SystemAddressDeployer{ + // Mainnet + 10: { + common.HexToAddress("0xcDE47C1a5e2d60b9ff262b0a3b6d486048575Ad9"), + common.HexToAddress("0x53A6eecC2dD4795Fcc68940ddc6B4d53Bd88Bd9E"), + }, + + // Kovan + 69: { + common.HexToAddress("0xd23eb5c2dd7035e6eb4a7e129249d9843123079f"), + common.HexToAddress("0xa81224490b9fa4930a2e920550cd1c9106bb6d9e"), + }, + + // Goerli + 420: { + common.HexToAddress("0xc30276833798867c1dbc5c468bf51ca900b44e4c"), + common.HexToAddress("0x5c679a57e018f5f146838138d3e032ef4913d551"), + }, + + // Goerli nightly + 421: { + common.HexToAddress("0xc30276833798867c1dbc5c468bf51ca900b44e4c"), + common.HexToAddress("0x5c679a57e018f5f146838138d3e032ef4913d551"), + }, +} + +var envSystemAddressDeployer SystemAddressDeployer + +func initEnvSystemAddressDeployer() { + deployer0Env := os.Getenv("SYSTEM_ADDRESS_0_DEPLOYER") + deployer1Env := os.Getenv("SYSTEM_ADDRESS_1_DEPLOYER") + + if deployer0Env == "" && deployer1Env == "" { + return + } + if !common.IsHexAddress(deployer0Env) { + panic("SYSTEM_ADDRESS_0_DEPLOYER specified but invalid") + } + if !common.IsHexAddress(deployer1Env) { + panic("SYSTEM_ADDRESS_1_DEPLOYER specified but invalid") + } + envSystemAddressDeployer[0] = common.HexToAddress(deployer0Env) + envSystemAddressDeployer[1] = common.HexToAddress(deployer1Env) +} + +func init() { + initEnvSystemAddressDeployer() +} diff --git a/l2geth/rollup/rcfg/system_address_test.go b/l2geth/rollup/rcfg/system_address_test.go new file mode 100644 index 0000000000..69ef0ab59a --- /dev/null +++ b/l2geth/rollup/rcfg/system_address_test.go @@ -0,0 +1,185 @@ +package rcfg + +import ( + "crypto/rand" + "fmt" + "math/big" + "os" + "testing" + + "github.com/ethereum-optimism/optimism/l2geth/common" +) + +func TestSystemAddressFor(t *testing.T) { + tests := []struct { + deployer0 common.Address + deployer1 common.Address + chainId int64 + }{ + { + common.HexToAddress("0xcDE47C1a5e2d60b9ff262b0a3b6d486048575Ad9"), + common.HexToAddress("0x53A6eecC2dD4795Fcc68940ddc6B4d53Bd88Bd9E"), + 10, + }, + { + common.HexToAddress("0xd23eb5c2dd7035e6eb4a7e129249d9843123079f"), + common.HexToAddress("0xa81224490b9fa4930a2e920550cd1c9106bb6d9e"), + 69, + }, + { + common.HexToAddress("0xc30276833798867c1dbc5c468bf51ca900b44e4c"), + common.HexToAddress("0x5c679a57e018f5f146838138d3e032ef4913d551"), + 420, + }, + { + common.HexToAddress("0xc30276833798867c1dbc5c468bf51ca900b44e4c"), + common.HexToAddress("0x5c679a57e018f5f146838138d3e032ef4913d551"), + 421, + }, + } + for _, tt := range tests { + chainID := big.NewInt(tt.chainId) + sad0 := SystemAddressFor(chainID, tt.deployer0) + if sad0 != SystemAddress0 { + t.Fatalf("expected %s, got %s", SystemAddress0.String(), sad0.String()) + } + sad1 := SystemAddressFor(chainID, tt.deployer1) + if sad1 != SystemAddress1 { + t.Fatalf("expected %s, got %s", SystemAddress1.String(), sad1.String()) + } + if SystemAddressFor(chainID, randAddr()) != ZeroSystemAddress { + t.Fatalf("expected zero address, but got a non-zero one instead") + } + } + + // test env fallback + addr0 := randAddr() + addr1 := randAddr() + chainID := big.NewInt(999) + if SystemAddressFor(chainID, addr0) != ZeroSystemAddress { + t.Fatalf("expected zero address, but got a non-zero one instead") + } + if SystemAddressFor(chainID, addr1) != ZeroSystemAddress { + t.Fatalf("expected zero address, but got a non-zero one instead") + } + if err := os.Setenv("SYSTEM_ADDRESS_0_DEPLOYER", addr0.String()); err != nil { + t.Fatalf("error setting env for deployer 0: %v", err) + } + if err := os.Setenv("SYSTEM_ADDRESS_1_DEPLOYER", addr1.String()); err != nil { + t.Fatalf("error setting env for deployer 1: %v", err) + } + initEnvSystemAddressDeployer() + sad0 := SystemAddressFor(chainID, addr0) + if sad0 != SystemAddress0 { + t.Fatalf("expected %s, got %s", SystemAddress0.String(), sad0.String()) + } + sad1 := SystemAddressFor(chainID, addr1) + if sad1 != SystemAddress1 { + t.Fatalf("expected %s, got %s", SystemAddress1.String(), sad1.String()) + } + + // reset + if err := os.Setenv("SYSTEM_ADDRESS_0_DEPLOYER", ""); err != nil { + t.Fatalf("error setting env for deployer 0: %v", err) + } + if err := os.Setenv("SYSTEM_ADDRESS_1_DEPLOYER", ""); err != nil { + t.Fatalf("error setting env for deployer 1: %v", err) + } + initEnvSystemAddressDeployer() +} + +func TestSystemAddressDeployer(t *testing.T) { + addr0 := randAddr() + addr1 := randAddr() + deployer := SystemAddressDeployer{addr0, addr1} + + assertAddress(t, deployer, addr0, SystemAddress0) + assertAddress(t, deployer, addr1, SystemAddress1) + assertAddress(t, deployer, randAddr(), ZeroSystemAddress) + + var zeroDeployer SystemAddressDeployer + assertAddress(t, zeroDeployer, randAddr(), ZeroSystemAddress) +} + +func TestEnvSystemAddressDeployer(t *testing.T) { + addr0 := randAddr() + addr1 := randAddr() + + assertAddress(t, envSystemAddressDeployer, addr0, ZeroSystemAddress) + assertAddress(t, envSystemAddressDeployer, addr1, ZeroSystemAddress) + assertAddress(t, envSystemAddressDeployer, randAddr(), ZeroSystemAddress) + + if err := os.Setenv("SYSTEM_ADDRESS_0_DEPLOYER", addr0.String()); err != nil { + t.Fatalf("error setting env for deployer 0: %v", err) + } + if err := os.Setenv("SYSTEM_ADDRESS_1_DEPLOYER", addr1.String()); err != nil { + t.Fatalf("error setting env for deployer 1: %v", err) + } + + initEnvSystemAddressDeployer() + assertAddress(t, envSystemAddressDeployer, addr0, SystemAddress0) + assertAddress(t, envSystemAddressDeployer, addr1, SystemAddress1) + assertAddress(t, envSystemAddressDeployer, randAddr(), ZeroSystemAddress) + + tests := []struct { + deployer0 string + deployer1 string + msg string + }{ + { + "not an address", + addr0.String(), + "SYSTEM_ADDRESS_0_DEPLOYER specified but invalid", + }, + { + "not an address", + "not an address", + "SYSTEM_ADDRESS_0_DEPLOYER specified but invalid", + }, + { + addr0.String(), + "not an address", + "SYSTEM_ADDRESS_1_DEPLOYER specified but invalid", + }, + } + for _, tt := range tests { + if err := os.Setenv("SYSTEM_ADDRESS_0_DEPLOYER", tt.deployer0); err != nil { + t.Fatalf("error setting env for deployer 0: %v", err) + } + if err := os.Setenv("SYSTEM_ADDRESS_1_DEPLOYER", tt.deployer1); err != nil { + t.Fatalf("error setting env for deployer 1: %v", err) + } + assertPanic(t, tt.msg, func() { + initEnvSystemAddressDeployer() + }) + } +} + +func randAddr() common.Address { + buf := make([]byte, 20) + _, err := rand.Read(buf) + if err != nil { + panic(err) + } + return common.BytesToAddress(buf) +} + +func assertAddress(t *testing.T, deployer SystemAddressDeployer, in common.Address, expected common.Address) { + actual := deployer.SystemAddressFor(in) + if actual != expected { + t.Fatalf("bad system address. expected %s, got %s", expected.String(), actual.String()) + } +} + +func assertPanic(t *testing.T, msg string, cb func()) { + defer func() { + if err := recover(); err != nil { + errMsg := fmt.Sprintf("%v", err) + if errMsg != msg { + t.Fatalf("expected error message %s, got %v", msg, errMsg) + } + } + }() + + cb() +} diff --git a/l2geth/rollup/sync_service.go b/l2geth/rollup/sync_service.go index a6d966d42f..a4a51f9d4e 100644 --- a/l2geth/rollup/sync_service.go +++ b/l2geth/rollup/sync_service.go @@ -171,20 +171,21 @@ func NewSyncService(ctx context.Context, cfg Config, txpool *core.TxPool, bc *co break } } - - // Wait until the remote service is done syncing - tStatus := time.NewTicker(10 * time.Second) - for ; true; <-tStatus.C { - status, err := service.client.SyncStatus(service.backend) - if err != nil { - log.Error("Cannot get sync status") - continue - } - if !status.Syncing { - tStatus.Stop() - break + if !cfg.IsVerifier || cfg.Backend == BackendL2 { + // Wait until the remote service is done syncing + tStatus := time.NewTicker(10 * time.Second) + for ; true; <-tStatus.C { + status, err := service.client.SyncStatus(service.backend) + if err != nil { + log.Error("Cannot get sync status") + continue + } + if !status.Syncing { + tStatus.Stop() + break + } + log.Info("Still syncing", "index", status.CurrentTransactionIndex, "tip", status.HighestKnownTransactionIndex) } - log.Info("Still syncing", "index", status.CurrentTransactionIndex, "tip", status.HighestKnownTransactionIndex) } // Initialize the latest L1 data here to make sure that diff --git a/ops_boba/api/watcher-api/serverless-mainnet.yml b/ops_boba/api/watcher-api/serverless-mainnet.yml index 3800e9320d..17931e431e 100644 --- a/ops_boba/api/watcher-api/serverless-mainnet.yml +++ b/ops_boba/api/watcher-api/serverless-mainnet.yml @@ -201,3 +201,20 @@ functions: cors: true layers: - ${file(env-mainnet.yml):LAYERS} + watcher_getLayerZeroTransactions: + handler: watcher_getLayerZeroTransactions.watcher_getLayerZeroTransactions + memorySize: 10240 # optional, in MB, default is 1024 + timeout: 60 # optional, in seconds, default is 6 + vpc: + securityGroupIds: + - ${file(env-mainnet.yml):SECURITY_GROUPS} + subnetIds: + - ${file(env-mainnet.yml):SUBNET_ID_1} + - ${file(env-mainnet.yml):SUBNET_ID_2} + events: + - http: + path: get.layerzero.transactions + method: post + cors: true + layers: + - ${file(env-mainnet.yml):LAYERS} diff --git a/ops_boba/api/watcher-api/watcher_getLayerZeroTransactions.py b/ops_boba/api/watcher-api/watcher_getLayerZeroTransactions.py index 734c723119..6c5d14904f 100644 --- a/ops_boba/api/watcher-api/watcher_getLayerZeroTransactions.py +++ b/ops_boba/api/watcher-api/watcher_getLayerZeroTransactions.py @@ -3,7 +3,7 @@ import pymysql -def watcher_getLayerZeroTransaction(event, context): +def watcher_getLayerZeroTransactions(event, context): # Parse incoming event body = json.loads(event["body"]) address = body.get("address") @@ -29,11 +29,9 @@ def watcher_getLayerZeroTransaction(event, context): cur.execute("""SELECT chainID, targetChainID, hash, blockNumber, amount, event, timestamp, reference FROM layerZeroTx - WHERE `crossTxFrom`=%s AND blockNumber <= %s AND blockNumber >= %s ORDER BY blockNumber""", (address, toRange, fromRange)) + WHERE `crossTxFrom`=%s ORDER BY CAST(blockNumber as unsigned) DESC LIMIT %s OFFSET %s""", (address, toRange - fromRange, fromRange)) transactionsDataRaw = cur.fetchall() - print("total", len(transactionsDataRaw)) for transactionDataRaw in transactionsDataRaw: - print("FOUND") transactionData.append({ "tx_hash": transactionDataRaw[2], "amount": transactionDataRaw[4], diff --git a/ops_boba/monitor/services/database.service.js b/ops_boba/monitor/services/database.service.js index e98eb42dd8..1373aed226 100644 --- a/ops_boba/monitor/services/database.service.js +++ b/ops_boba/monitor/services/database.service.js @@ -159,7 +159,7 @@ class DatabaseService extends OptimismEnv { crossTxTo VARCHAR(255), amount VARCHAR(255), event VARCHAR(255), - reference VARCAR(255), + reference VARCHAR(255), PRIMARY KEY ( hash, blockNumber) )`) con.end() diff --git a/ops_boba/monitor/services/layerZeroBridge.js b/ops_boba/monitor/services/layerZeroBridge.js index 1b9f481937..25d332194d 100644 --- a/ops_boba/monitor/services/layerZeroBridge.js +++ b/ops_boba/monitor/services/layerZeroBridge.js @@ -61,7 +61,7 @@ class LayerZeroBridgeMonitor extends OptimismEnv { for (let i = 0; i < this.layerZeroBridges.length; i++) { const bridgeName = this.layerZeroBridges[i] const isFromETH = bridgeName.search(/EthBridgeTo/) > -1 - const abi = isFromETH ? EthBridgeJson.abi : AltLBridge.abi + const abi = isFromETH ? EthBridgeJson.abi : AltL1Bridge.abi const contract = new ethers.Contract( bridgeAddresses[i], abi, @@ -107,10 +107,8 @@ class LayerZeroBridgeMonitor extends OptimismEnv { } async scanBlockRange(startBlock, endBlock) { - console.log(prefix, `scan from block ${startBlock} to block ${endBlock}`) for (let i = startBlock; i <= endBlock; i += 1000) { const upperBlock = Math.min(i + 999, endBlock) - console.log(prefix, `scan blockRange`, i, upperBlock) for (let j = 0; j < this.bridgeContracts.length; j++) { await this.scanBlock( @@ -128,19 +126,43 @@ class LayerZeroBridgeMonitor extends OptimismEnv { } async scanBlock(startBlock, endBlock, bridgeName, bridgeContract, dstChainID) { - const logs = await bridgeContract.queryFilter( - bridgeName.search(/EthBridgeTo/) > -1 - ? [ + const getEvents = async (events, startBlock, endBlock) => { + let logs = [] + for (const event of events) { + const log = await bridgeContract.queryFilter( + event, + Number(startBlock), + Number(endBlock) + ) + logs = logs.concat(log) + } + return logs + } + + let logs = [] + if (bridgeName.search(/EthBridgeTo/) > -1) { + logs = await getEvents( + [ bridgeContract.filters.ERC20DepositInitiated(), bridgeContract.filters.ERC20WithdrawalFinalized(), - ] - : [ + ], + startBlock, + endBlock + ) + } else { + logs = await getEvents( + [ bridgeContract.filters.WithdrawalInitiated(), bridgeContract.filters.DepositFinalized(), ], - Number(startBlock), - Number(endBlock) - ) + startBlock, + endBlock + ) + } + + if (logs.length !== 0) { + console.log(prefix, `found events from ${startBlock} to ${endBlock}`) + } for (const l of logs) { const eventNames = [ @@ -159,7 +181,7 @@ class LayerZeroBridgeMonitor extends OptimismEnv { const result = await response.json() let url = '' - if (len(result.messages) > 0) { + if (result.messages.length > 0) { const message = result.messages[0] // [ // { @@ -204,8 +226,6 @@ class LayerZeroBridgeMonitor extends OptimismEnv { } } - - errorCatcher(func, param) { return (async () => { for (let i = 0; i < 2; i++) { diff --git a/ops_boba/monitor/services/utilities/optimismEnv.js b/ops_boba/monitor/services/utilities/optimismEnv.js index 51df56ea34..7a9e8690a8 100644 --- a/ops_boba/monitor/services/utilities/optimismEnv.js +++ b/ops_boba/monitor/services/utilities/optimismEnv.js @@ -82,7 +82,8 @@ const L1_BLOCK_CONFIRMATION = env.L1_BLOCK_CONFIRMATION || 0 const NUMBER_OF_BLOCKS_TO_FETCH = env.NUMBER_OF_BLOCKS_TO_FETCH || 10000000 // layerZero env -const LAYER_ZERO_ENABLE_TEST = Boolean(env.LAYER_ZERO_ENABLE_TEST) || true +const LAYER_ZERO_ENABLE_TEST = + env.LAYER_ZERO_ENABLE_TEST === 'true' ? true : false const LAYER_ZERO_CHAIN = env.LAYER_ZERO_CHAIN || 'Testnet' const LAYER_ZERO_BRIDGES = env.LAYER_ZERO_BRIDGES || 'Proxy__EthBridgeToAvalanche' const LAYER_ZERO_LATEST_BLOCK = Number(env.LAYER_ZERO_LATEST_BLOCK) || 0 diff --git a/packages/boba/contracts/contracts/bridges/L1NFTBridge.sol b/packages/boba/contracts/contracts/bridges/L1NFTBridge.sol index a36f35c6b3..85fa768086 100644 --- a/packages/boba/contracts/contracts/bridges/L1NFTBridge.sol +++ b/packages/boba/contracts/contracts/bridges/L1NFTBridge.sol @@ -1,5 +1,9 @@ // SPDX-License-Identifier: MIT // @unsupported: ovm + +/** + Note: This contract has not been audited, exercise caution when using this on mainnet + */ pragma solidity >0.7.5; pragma experimental ABIEncoderV2; @@ -407,6 +411,8 @@ contract L1NFTBridge is iL1NFTBridge, CrossDomainEnabled, ERC721Holder, Reentran emit NFTWithdrawalFinalized(_l1Contract, _l2Contract, _from, _to, _tokenId, _data); } else { + // replyNeeded helps store the status if a message needs to be sent back to the other layer + bool replyNeeded = false; // Check the target token is compliant and // verify the deposited token on L2 matches the L1 deposited token representation here if ( @@ -416,9 +422,16 @@ contract L1NFTBridge is iL1NFTBridge, CrossDomainEnabled, ERC721Holder, Reentran ) { // When a deposit is finalized, we credit the account on L2 with the same amount of // tokens. - IL1StandardERC721(_l1Contract).mint(_to, _tokenId, _data); - emit NFTWithdrawalFinalized(_l1Contract, _l2Contract, _from, _to, _tokenId, _data); + try IL1StandardERC721(_l1Contract).mint(_to, _tokenId, _data) { + emit NFTWithdrawalFinalized(_l1Contract, _l2Contract, _from, _to, _tokenId, _data); + } catch { + replyNeeded = true; + } } else { + replyNeeded = true; + } + + if (replyNeeded) { bytes memory message = abi.encodeWithSelector( iL2NFTBridge.finalizeDeposit.selector, _l1Contract, diff --git a/packages/boba/contracts/contracts/bridges/L2NFTBridge.sol b/packages/boba/contracts/contracts/bridges/L2NFTBridge.sol index 8e9a43185b..a98fab7a3f 100644 --- a/packages/boba/contracts/contracts/bridges/L2NFTBridge.sol +++ b/packages/boba/contracts/contracts/bridges/L2NFTBridge.sol @@ -1,4 +1,8 @@ // SPDX-License-Identifier: MIT + +/** + Note: This contract has not been audited, exercise caution when using this on mainnet + */ pragma solidity >0.7.5; pragma experimental ABIEncoderV2; @@ -461,6 +465,8 @@ contract L2NFTBridge is iL2NFTBridge, CrossDomainEnabled, ERC721Holder, Reentran PairNFTInfo storage pairNFT = pairNFTInfo[_l2Contract]; if (pairNFT.baseNetwork == Network.L1) { + // replyNeeded helps store the status if a message needs to be sent back to the other layer + bool replyNeeded = false; // Check the target token is compliant and // verify the deposited token on L1 matches the L2 deposited token representation here if ( @@ -470,11 +476,18 @@ contract L2NFTBridge is iL2NFTBridge, CrossDomainEnabled, ERC721Holder, Reentran ) { // When a deposit is finalized, we credit the account on L2 with the same amount of // tokens. - IL2StandardERC721(_l2Contract).mint(_to, _tokenId, _data); - emit DepositFinalized(_l1Contract, _l2Contract, _from, _to, _tokenId, _data); + try IL2StandardERC721(_l2Contract).mint(_to, _tokenId, _data) { + emit DepositFinalized(_l1Contract, _l2Contract, _from, _to, _tokenId, _data); + } catch { + replyNeeded = true; + } } else { + replyNeeded = true; + } + + if (replyNeeded) { // Either the L2 token which is being deposited-into disagrees about the correct address - // of its L1 token, or does not support the correct interface. + // of its L1 token, or does not support the correct interface, or maybe the l2 mint reverted // This should only happen if there is a malicious L2 token, or if a user somehow // specified the wrong L2 token address to deposit into. // In either case, we stop the process here and construct a withdrawal diff --git a/packages/boba/contracts/contracts/bridges/README.md b/packages/boba/contracts/contracts/bridges/README.md index d39f22c1d9..9e1635e424 100644 --- a/packages/boba/contracts/contracts/bridges/README.md +++ b/packages/boba/contracts/contracts/bridges/README.md @@ -2,6 +2,8 @@ Boba NFT Bridge +> **Note: These contracts have not been audited, exercise caution when using them on mainnet** + Boba NFT bridges support **native L1 NFTs** and **native L2 NFTs** to be moved back and forth. * Native L1 NFT: the original NFT contract was deployed on L1 diff --git a/packages/boba/contracts/contracts/lzTokenBridge/AltL1Bridge.sol b/packages/boba/contracts/contracts/lzTokenBridge/AltL1Bridge.sol new file mode 100644 index 0000000000..777cd6199b --- /dev/null +++ b/packages/boba/contracts/contracts/lzTokenBridge/AltL1Bridge.sol @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +/* Library Imports */ +import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import { IL2StandardERC20 } from "@eth-optimism/contracts/contracts/standards/IL2StandardERC20.sol"; + +import { IAltL1Bridge } from "./interfaces/IAltL1Bridge.sol"; +import "./lzApp/NonblockingLzApp.sol"; + +/** + * @title AltL1Bridge + * @dev The AltL1Bridge is a contract which works together with the EthBridge to + * enable ERC20 transitions between layers + * This contract acts as a minter for new tokens when it hears about deposits into the EthBridge. + * This contract also acts as a burner of the tokens intended for withdrawal, informing the + * EthBridge to release L1 funds. + * + * Runtime target: EVM + */ + contract AltL1Bridge is IAltL1Bridge, NonblockingLzApp { + + // set l1(eth) bridge address as setTrustedDomain() + // dstChainId is primarily ethereum + uint16 public dstChainId; + + // set allowance for custom gas limit + bool public useCustomAdapterParams; + uint public constant NO_EXTRA_GAS = 0; + uint public constant FUNCTION_TYPE_SEND = 1; + + // set maximum amount of tokens can be transferred in 24 hours + uint256 public maxTransferAmountPerDay; + uint256 public transferredAmount; + uint256 public transferTimestampCheckPoint; + + // Note: Specify the _lzEndpoint on this layer, _dstChainId is not the actual evm chainIds, but the layerZero + // proprietary ones, pass the chainId of the destination for _dstChainId + function initialize(address _lzEndpoint, uint16 _dstChainId, address _ethBridgeAddress) public initializer { + require(_lzEndpoint != address(0), "lz endpoint cannot be zero address"); + + __NonblockingLzApp_init(_lzEndpoint); + // allow only a specific destination + dstChainId = _dstChainId; + // set l1(eth) bridge address on destination as setTrustedDomain() + setTrustedRemote(_dstChainId, abi.encodePacked(_ethBridgeAddress)); + + // set maximum amount of tokens can be transferred in 24 hours + transferTimestampCheckPoint = block.timestamp; + maxTransferAmountPerDay = 500_000e18; + } + + /*************** + * Withdrawing * + ***************/ + + function withdraw( + address _l2Token, + uint256 _amount, + address _zroPaymentAddress, + bytes memory _adapterParams, + bytes calldata _data + ) external virtual payable { + _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _zroPaymentAddress, _adapterParams, _data); + } + + function withdrawTo( + address _l2Token, + address _to, + uint256 _amount, + address _zroPaymentAddress, + bytes memory _adapterParams, + bytes calldata _data + ) external virtual payable { + _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _zroPaymentAddress, _adapterParams, _data); + } + + function _initiateWithdrawal( + address _l2Token, + address _from, + address _to, + uint256 _amount, + address _zroPaymentAddress, + bytes memory _adapterParams, + bytes calldata _data + ) internal { + require(_to != address(0), "_to cannot be zero address"); + + // check if the total amount transferred is smaller than the maximum amount of tokens can be transferred in 24 hours + // if it's out of 24 hours, reset the transferred amount to 0 and set the transferTimestampCheckPoint to the current time + if (block.timestamp < transferTimestampCheckPoint + 86400) { + transferredAmount += _amount; + require(transferredAmount <= maxTransferAmountPerDay, "max amount per day exceeded"); + } else { + transferredAmount = _amount; + require(transferredAmount <= maxTransferAmountPerDay, "max amount per day exceeded"); + transferTimestampCheckPoint = block.timestamp; + } + + // When a withdrawal is initiated, we burn the withdrawer's funds to prevent subsequent L2 + // usage + IL2StandardERC20(_l2Token).burn(msg.sender, _amount); + + // Construct calldata for l1TokenBridge.finalizeERC20Withdrawal(_to, _amount) + address l1Token = IL2StandardERC20(_l2Token).l1Token(); + + bytes memory payload = abi.encode( + l1Token, + _l2Token, + _from, + _to, + _amount, + _data + ); + if (useCustomAdapterParams) { + _checkGasLimit(dstChainId, FUNCTION_TYPE_SEND, _adapterParams, NO_EXTRA_GAS); + } else { + require(_adapterParams.length == 0, "LzApp: _adapterParams must be empty."); + } + + // Send payload to Ethereum + _lzSend(dstChainId, payload, payable(msg.sender), _zroPaymentAddress, _adapterParams); + + emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data); + } + + /************************************ + * Cross-chain Function: Depositing * + ************************************/ + + function _nonblockingLzReceive( + uint16 _srcChainId, + bytes memory _srcAddress, + uint64 _nonce, + bytes memory _payload + ) internal override { + // sanity check + // _srcAddress is already checked on an upper level + require(_srcChainId == dstChainId, "Invalid source chainId"); + ( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + bytes memory _data + ) = abi.decode(_payload, (address, address, address, address, uint256, bytes)); + + // Check the target token is compliant and + // verify the deposited token on L1 matches the L2 deposited token representation here + if ( + ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) && + _l1Token == IL2StandardERC20(_l2Token).l1Token() + ) { + // When a deposit is finalized, we credit the account on L2 with the same amount of + // tokens. + IL2StandardERC20(_l2Token).mint(_to, _amount); + emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data); + } else { + // Either the L2 token which is being deposited-into disagrees about the correct address + // of its L1 token, or does not support the correct interface. + // This should only happen if there is a malicious L2 token, or if a user somehow + // specified the wrong L2 token address to deposit into. + // In either case, we stop the process here and construct a withdrawal + // message so that users can get their funds out in some cases. + // since, sending messages will need fees to be paid in the native token, this call cannot succeed directly + // users, will have to call retryMessage with paying appropriate fee + bytes memory payload = abi.encode( + _l1Token, + _l2Token, + _to, // _to and _from interchanged + _from, + _amount, + _data + ); + + // this is going to fail on the original relay, to get refund back in this case, user would + // have to call retryMessage, also paying for the xMessage fee + // custom adapters would not be applicable for this + _lzSend(dstChainId, payload, payable(msg.sender), address(0x0), bytes("")); + } + } + + /************** + * Admin * + **************/ + + function setDstChainId(uint16 _dstChainId) external onlyOwner { + dstChainId = _dstChainId; + } + + function setUseCustomAdapterParams(bool _useCustomAdapterParams, uint _dstGasAmount) external onlyOwner() { + useCustomAdapterParams = _useCustomAdapterParams; + // set dstGas lookup, since only one dstchainId is allowed and its known + setMinDstGasLookup(dstChainId, FUNCTION_TYPE_SEND, _dstGasAmount); + } + + function setMaxTransferAmountPerDay(uint256 _maxTransferAmountPerDay) external onlyOwner() { + maxTransferAmountPerDay = _maxTransferAmountPerDay; + } + } diff --git a/packages/boba/contracts/contracts/lzTokenBridge/EthBridge.sol b/packages/boba/contracts/contracts/lzTokenBridge/EthBridge.sol new file mode 100644 index 0000000000..abd8b1938f --- /dev/null +++ b/packages/boba/contracts/contracts/lzTokenBridge/EthBridge.sol @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import { IEthBridge } from "./interfaces/IEthBridge.sol"; +import "./lzApp/NonblockingLzApp.sol"; + +/** + * @title EthBridge + * @dev EthBridge is a contract on Ethereum which stores deposited L1 funds (ERC20s) in order to be wrapped + * and used on other L1s. It uses LayerZero messages to synchronize a corresponding Bridge on the + * other layer, informing it of deposits and listening to it for newly finalized withdrawals. + * + * Runtime target: EVM + */ + contract EthBridge is IEthBridge, NonblockingLzApp { + using SafeERC20 for IERC20; + + // set altl1 bridge address as setTrustedDomain() + uint16 public dstChainId; + + // set allowance for custom gas limit + bool public useCustomAdapterParams; + uint public constant NO_EXTRA_GAS = 0; + uint public constant FUNCTION_TYPE_SEND = 1; + + // set maximum amount of tokens can be transferred in 24 hours + uint256 public maxTransferAmountPerDay; + uint256 public transferredAmount; + uint256 public transferTimestampCheckPoint; + + // Maps L1 token to wrapped token on alt l1 to balance of the L1 token deposited + mapping(address => mapping(address => uint256)) public deposits; + + // Note: Specify the _lzEndpoint on this layer, _dstChainId is not the actual evm chainIds, but the layerZero + // proprietary ones, pass the chainId of the destination for _dstChainId + function initialize(address _lzEndpoint, uint16 _dstChainId, address _altL1BridgeAddress) public initializer { + require(_lzEndpoint != address(0), "lz endpoint cannot be zero address"); + + __NonblockingLzApp_init(_lzEndpoint); + // allow only a specific destination + dstChainId = _dstChainId; + // set altl1 bridge address on destination as setTrustedDomain() + setTrustedRemote(_dstChainId, abi.encodePacked(_altL1BridgeAddress)); + + // set maximum amount of tokens can be transferred in 24 hours + transferTimestampCheckPoint = block.timestamp; + maxTransferAmountPerDay = 500_000e18; + } + + /************** + * Depositing * + **************/ + + /** @dev Modifier requiring sender to be EOA. This check could be bypassed by a malicious + * contract via initcode, but it takes care of the user error we want to avoid. + */ + modifier onlyEOA() { + // Used to stop deposits from contracts (avoid accidentally lost tokens) + require(!Address.isContract(msg.sender), "Account not EOA"); + _; + } + + function depositERC20( + address _l1Token, + address _l2Token, + uint256 _amount, + address _zroPaymentAddress, + bytes memory _adapterParams, + bytes calldata _data + ) external virtual payable onlyEOA { + _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _zroPaymentAddress, _adapterParams, _data); + } + + function depositERC20To( + address _l1Token, + address _l2Token, + address _to, + uint256 _amount, + address _zroPaymentAddress, + bytes memory _adapterParams, + bytes calldata _data + ) external virtual payable { + _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _zroPaymentAddress, _adapterParams, _data); + } + + // add nonreentrant + function _initiateERC20Deposit( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + address _zroPaymentAddress, + bytes memory _adapterParams, + bytes calldata _data + ) internal { + require(_to != address(0), "_to cannot be zero address"); + + // check if the total amount transferred is smaller than the maximum amount of tokens can be transferred in 24 hours + // if it's out of 24 hours, reset the transferred amount to 0 and set the transferTimestampCheckPoint to the current time + if (block.timestamp < transferTimestampCheckPoint + 86400) { + transferredAmount += _amount; + require(transferredAmount <= maxTransferAmountPerDay, "max amount per day exceeded"); + } else { + transferredAmount = _amount; + require(transferredAmount <= maxTransferAmountPerDay, "max amount per day exceeded"); + transferTimestampCheckPoint = block.timestamp; + } + + // When a deposit is initiated on Ethereum, the Eth Bridge transfers the funds to itself for future + // withdrawals. safeTransferFrom also checks if the contract has code, so this will fail if + // _from is an EOA or address(0). + IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount); + + // construct payload to send, we don't explicitly specify the action, the receive on the + // other side should expect only this action + bytes memory payload = abi.encode( + _l1Token, + _l2Token, + _from, + _to, + _amount, + _data + ); + if (useCustomAdapterParams) { + _checkGasLimit(dstChainId, FUNCTION_TYPE_SEND, _adapterParams, NO_EXTRA_GAS); + } else { + require(_adapterParams.length == 0, "LzApp: _adapterParams must be empty."); + } + + deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount; + + // Send payload to the other L1 + _lzSend(dstChainId, payload, payable(msg.sender), _zroPaymentAddress, _adapterParams); + + emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data); + } + + /************** + * Withdrawing * + **************/ + + function _nonblockingLzReceive( + uint16 _srcChainId, + bytes memory _srcAddress, + uint64 _nonce, + bytes memory _payload + ) internal override { + // sanity check + // _srcAddress is already checked on an upper level + require(_srcChainId == dstChainId, "Invalid source chainId"); + ( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + bytes memory _data + ) = abi.decode(_payload, (address, address, address, address, uint256, bytes)); + + deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] - _amount; + IERC20(_l1Token).safeTransfer(_to, _amount); + + emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data); + } + + /************** + * Admin * + **************/ + + function setDstChainId(uint16 _dstChainId) external onlyOwner { + dstChainId = _dstChainId; + } + + function setUseCustomAdapterParams(bool _useCustomAdapterParams, uint _dstGasAmount) external onlyOwner() { + useCustomAdapterParams = _useCustomAdapterParams; + // set dstGas lookup, since only one dstchainId is allowed and its known + setMinDstGasLookup(dstChainId, FUNCTION_TYPE_SEND, _dstGasAmount); + } + + function setMaxTransferAmountPerDay(uint256 _maxTransferAmountPerDay) external onlyOwner() { + maxTransferAmountPerDay = _maxTransferAmountPerDay; + } +} diff --git a/packages/boba/contracts/contracts/lzTokenBridge/interfaces/IAltL1Bridge.sol b/packages/boba/contracts/contracts/lzTokenBridge/interfaces/IAltL1Bridge.sol new file mode 100644 index 0000000000..b41ccb5fda --- /dev/null +++ b/packages/boba/contracts/contracts/lzTokenBridge/interfaces/IAltL1Bridge.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +interface IAltL1Bridge { + event WithdrawalInitiated( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + event DepositFinalized( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + function withdraw( + address _l2Token, + uint256 _amount, + address _zroPaymentAddress, + bytes memory _adapterParams, + bytes calldata _data + ) external payable; + + function withdrawTo( + address _l2Token, + address _to, + uint256 _amount, + address _zroPaymentAddress, + bytes memory _adapterParams, + bytes calldata _data + ) external payable; +} \ No newline at end of file diff --git a/packages/boba/contracts/contracts/lzTokenBridge/interfaces/IEthBridge.sol b/packages/boba/contracts/contracts/lzTokenBridge/interfaces/IEthBridge.sol new file mode 100644 index 0000000000..951a55c1dd --- /dev/null +++ b/packages/boba/contracts/contracts/lzTokenBridge/interfaces/IEthBridge.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +interface IEthBridge { + event ERC20DepositInitiated( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + event ERC20WithdrawalFinalized( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + function depositERC20( + address _l1Token, + address _l2Token, + uint256 _amount, + address _zroPaymentAddress, + bytes memory _adapterParams, + bytes calldata _data + ) external payable; + + function depositERC20To( + address _l1Token, + address _l2Token, + address _to, + uint256 _amount, + address _zroPaymentAddress, + bytes memory _adapterParams, + bytes calldata _data + ) external payable; +} \ No newline at end of file diff --git a/packages/boba/contracts/contracts/lzTokenBridge/interfaces/ILayerZeroEndpoint.sol b/packages/boba/contracts/contracts/lzTokenBridge/interfaces/ILayerZeroEndpoint.sol new file mode 100644 index 0000000000..b7cb75cf06 --- /dev/null +++ b/packages/boba/contracts/contracts/lzTokenBridge/interfaces/ILayerZeroEndpoint.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.0; + +import "./ILayerZeroUserApplicationConfig.sol"; + +interface ILayerZeroEndpoint is ILayerZeroUserApplicationConfig { + // @notice send a LayerZero message to the specified address at a LayerZero endpoint. + // @param _dstChainId - the destination chain identifier + // @param _destination - the address on destination chain (in bytes). address length/format may vary by chains + // @param _payload - a custom bytes payload to send to the destination contract + // @param _refundAddress - if the source transaction is cheaper than the amount of value passed, refund the additional amount to this address + // @param _zroPaymentAddress - the address of the ZRO token holder who would pay for the transaction + // @param _adapterParams - parameters for custom functionality. e.g. receive airdropped native gas from the relayer on destination + function send(uint16 _dstChainId, bytes calldata _destination, bytes calldata _payload, address payable _refundAddress, address _zroPaymentAddress, bytes calldata _adapterParams) external payable; + + // @notice used by the messaging library to publish verified payload + // @param _srcChainId - the source chain identifier + // @param _srcAddress - the source contract (as bytes) at the source chain + // @param _dstAddress - the address on destination chain + // @param _nonce - the unbound message ordering nonce + // @param _gasLimit - the gas limit for external contract execution + // @param _payload - verified payload to send to the destination contract + function receivePayload(uint16 _srcChainId, bytes calldata _srcAddress, address _dstAddress, uint64 _nonce, uint _gasLimit, bytes calldata _payload) external; + + // @notice get the inboundNonce of a lzApp from a source chain which could be EVM or non-EVM chain + // @param _srcChainId - the source chain identifier + // @param _srcAddress - the source chain contract address + function getInboundNonce(uint16 _srcChainId, bytes calldata _srcAddress) external view returns (uint64); + + // @notice get the outboundNonce from this source chain which, consequently, is always an EVM + // @param _srcAddress - the source chain contract address + function getOutboundNonce(uint16 _dstChainId, address _srcAddress) external view returns (uint64); + + // @notice gets a quote in source native gas, for the amount that send() requires to pay for message delivery + // @param _dstChainId - the destination chain identifier + // @param _userApplication - the user app address on this EVM chain + // @param _payload - the custom message to send over LayerZero + // @param _payInZRO - if false, user app pays the protocol fee in native token + // @param _adapterParam - parameters for the adapter service, e.g. send some dust native token to dstChain + function estimateFees(uint16 _dstChainId, address _userApplication, bytes calldata _payload, bool _payInZRO, bytes calldata _adapterParam) external view returns (uint nativeFee, uint zroFee); + + // @notice get this Endpoint's immutable source identifier + function getChainId() external view returns (uint16); + + // @notice the interface to retry failed message on this Endpoint destination + // @param _srcChainId - the source chain identifier + // @param _srcAddress - the source chain contract address + // @param _payload - the payload to be retried + function retryPayload(uint16 _srcChainId, bytes calldata _srcAddress, bytes calldata _payload) external; + + // @notice query if any STORED payload (message blocking) at the endpoint. + // @param _srcChainId - the source chain identifier + // @param _srcAddress - the source chain contract address + function hasStoredPayload(uint16 _srcChainId, bytes calldata _srcAddress) external view returns (bool); + + // @notice query if the _libraryAddress is valid for sending msgs. + // @param _userApplication - the user app address on this EVM chain + function getSendLibraryAddress(address _userApplication) external view returns (address); + + // @notice query if the _libraryAddress is valid for receiving msgs. + // @param _userApplication - the user app address on this EVM chain + function getReceiveLibraryAddress(address _userApplication) external view returns (address); + + // @notice query if the non-reentrancy guard for send() is on + // @return true if the guard is on. false otherwise + function isSendingPayload() external view returns (bool); + + // @notice query if the non-reentrancy guard for receive() is on + // @return true if the guard is on. false otherwise + function isReceivingPayload() external view returns (bool); + + // @notice get the configuration of the LayerZero messaging library of the specified version + // @param _version - messaging library version + // @param _chainId - the chainId for the pending config change + // @param _userApplication - the contract address of the user application + // @param _configType - type of configuration. every messaging library has its own convention. + function getConfig(uint16 _version, uint16 _chainId, address _userApplication, uint _configType) external view returns (bytes memory); + + // @notice get the send() LayerZero messaging library version + // @param _userApplication - the contract address of the user application + function getSendVersion(address _userApplication) external view returns (uint16); + + // @notice get the lzReceive() LayerZero messaging library version + // @param _userApplication - the contract address of the user application + function getReceiveVersion(address _userApplication) external view returns (uint16); +} diff --git a/packages/boba/contracts/contracts/lzTokenBridge/interfaces/ILayerZeroReceiver.sol b/packages/boba/contracts/contracts/lzTokenBridge/interfaces/ILayerZeroReceiver.sol new file mode 100644 index 0000000000..9c117e68c1 --- /dev/null +++ b/packages/boba/contracts/contracts/lzTokenBridge/interfaces/ILayerZeroReceiver.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.0; + +interface ILayerZeroReceiver { + // @notice LayerZero endpoint will invoke this function to deliver the message on the destination + // @param _srcChainId - the source endpoint identifier + // @param _srcAddress - the source sending contract address from the source chain + // @param _nonce - the ordered message nonce + // @param _payload - the signed payload is the UA bytes has encoded to be sent + function lzReceive(uint16 _srcChainId, bytes calldata _srcAddress, uint64 _nonce, bytes calldata _payload) external; +} diff --git a/packages/boba/contracts/contracts/lzTokenBridge/interfaces/ILayerZeroUserApplicationConfig.sol b/packages/boba/contracts/contracts/lzTokenBridge/interfaces/ILayerZeroUserApplicationConfig.sol new file mode 100644 index 0000000000..297eff90fd --- /dev/null +++ b/packages/boba/contracts/contracts/lzTokenBridge/interfaces/ILayerZeroUserApplicationConfig.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.0; + +interface ILayerZeroUserApplicationConfig { + // @notice set the configuration of the LayerZero messaging library of the specified version + // @param _version - messaging library version + // @param _chainId - the chainId for the pending config change + // @param _configType - type of configuration. every messaging library has its own convention. + // @param _config - configuration in the bytes. can encode arbitrary content. + function setConfig(uint16 _version, uint16 _chainId, uint _configType, bytes calldata _config) external; + + // @notice set the send() LayerZero messaging library version to _version + // @param _version - new messaging library version + function setSendVersion(uint16 _version) external; + + // @notice set the lzReceive() LayerZero messaging library version to _version + // @param _version - new messaging library version + function setReceiveVersion(uint16 _version) external; + + // @notice Only when the UA needs to resume the message flow in blocking mode and clear the stored payload + // @param _srcChainId - the chainId of the source chain + // @param _srcAddress - the contract address of the source contract at the source chain + function forceResumeReceive(uint16 _srcChainId, bytes calldata _srcAddress) external; +} diff --git a/packages/boba/contracts/contracts/lzTokenBridge/lzApp/LzApp.sol b/packages/boba/contracts/contracts/lzTokenBridge/lzApp/LzApp.sol new file mode 100644 index 0000000000..21b1c960f4 --- /dev/null +++ b/packages/boba/contracts/contracts/lzTokenBridge/lzApp/LzApp.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "../interfaces/ILayerZeroReceiver.sol"; +import "../interfaces/ILayerZeroUserApplicationConfig.sol"; +import "../interfaces/ILayerZeroEndpoint.sol"; +import "./LzLib.sol"; + +/* + * a generic LzReceiver implementation + */ +abstract contract LzApp is OwnableUpgradeable, ILayerZeroReceiver, ILayerZeroUserApplicationConfig { + ILayerZeroEndpoint public lzEndpoint; + + mapping(uint16 => bytes) public trustedRemoteLookup; + mapping(uint16 => mapping(uint => uint)) public minDstGasLookup; + + event SetTrustedRemote(uint16 _srcChainId, bytes _srcAddress); + + function __LzApp_init(address _endpoint) internal initializer { + OwnableUpgradeable.__Ownable_init(); + + __LzApp_init_unchained(_endpoint); + } + + function __LzApp_init_unchained(address _endpoint) internal initializer { + lzEndpoint = ILayerZeroEndpoint(_endpoint); + } + + function lzReceive(uint16 _srcChainId, bytes memory _srcAddress, uint64 _nonce, bytes memory _payload) public virtual override { + // lzReceive must be called by the endpoint for security + require(_msgSender() == address(lzEndpoint), "LzApp: invalid endpoint caller"); + + bytes memory trustedRemote = trustedRemoteLookup[_srcChainId]; + // if will still block the message pathway from (srcChainId, srcAddress). should not receive message from untrusted remote. + require(_srcAddress.length == trustedRemote.length && keccak256(_srcAddress) == keccak256(trustedRemote), "LzApp: invalid source sending contract"); + + _blockingLzReceive(_srcChainId, _srcAddress, _nonce, _payload); + } + + // abstract function - the default behaviour of LayerZero is blocking. See: NonblockingLzApp if you dont need to enforce ordered messaging + function _blockingLzReceive(uint16 _srcChainId, bytes memory _srcAddress, uint64 _nonce, bytes memory _payload) internal virtual; + + function _lzSend(uint16 _dstChainId, bytes memory _payload, address payable _refundAddress, address _zroPaymentAddress, bytes memory _adapterParams) internal virtual { + bytes memory trustedRemote = trustedRemoteLookup[_dstChainId]; + require(trustedRemote.length != 0, "LzApp: destination chain is not a trusted source"); + lzEndpoint.send{value: msg.value}(_dstChainId, trustedRemote, _payload, _refundAddress, _zroPaymentAddress, _adapterParams); + } + + function _checkGasLimit(uint16 _dstChainId, uint _type, bytes memory _adapterParams, uint _extraGas) internal view { + uint providedGasLimit = LzLib.getGasLimit(_adapterParams); + uint minGasLimit = minDstGasLookup[_dstChainId][_type] + _extraGas; + require(minGasLimit > 0, "LzApp: minGasLimit not set"); + require(providedGasLimit >= minGasLimit, "LzApp: gas limit is too low"); + } + + //---------------------------UserApplication config---------------------------------------- + function getConfig(uint16 _version, uint16 _chainId, address, uint _configType) external view returns (bytes memory) { + return lzEndpoint.getConfig(_version, _chainId, address(this), _configType); + } + + // generic config for LayerZero user Application + function setConfig(uint16 _version, uint16 _chainId, uint _configType, bytes calldata _config) external override onlyOwner { + lzEndpoint.setConfig(_version, _chainId, _configType, _config); + } + + function setSendVersion(uint16 _version) external override onlyOwner { + lzEndpoint.setSendVersion(_version); + } + + function setReceiveVersion(uint16 _version) external override onlyOwner { + lzEndpoint.setReceiveVersion(_version); + } + + function forceResumeReceive(uint16 _srcChainId, bytes calldata _srcAddress) external override onlyOwner { + lzEndpoint.forceResumeReceive(_srcChainId, _srcAddress); + } + + // allow owner to set it multiple times. + function setTrustedRemote(uint16 _srcChainId, bytes memory _srcAddress) public onlyOwner { + trustedRemoteLookup[_srcChainId] = _srcAddress; + emit SetTrustedRemote(_srcChainId, _srcAddress); + } + + function setMinDstGasLookup(uint16 _dstChainId, uint _type, uint _dstGasAmount) public onlyOwner { + require(_dstGasAmount > 0, "LzApp: invalid _dstGasAmount"); + minDstGasLookup[_dstChainId][_type] = _dstGasAmount; + } + + //--------------------------- VIEW FUNCTION ---------------------------------------- + function isTrustedRemote(uint16 _srcChainId, bytes calldata _srcAddress) external view returns (bool) { + bytes memory trustedSource = trustedRemoteLookup[_srcChainId]; + return keccak256(trustedSource) == keccak256(_srcAddress); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} diff --git a/packages/boba/contracts/contracts/lzTokenBridge/lzApp/LzLib.sol b/packages/boba/contracts/contracts/lzTokenBridge/lzApp/LzLib.sol new file mode 100644 index 0000000000..b377bfcfdc --- /dev/null +++ b/packages/boba/contracts/contracts/lzTokenBridge/lzApp/LzLib.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +library LzLib { + function getGasLimit(bytes memory _adapterParams) internal pure returns (uint gasLimit) { + assembly { + gasLimit := mload(add(_adapterParams, 34)) + } + } +} diff --git a/packages/boba/contracts/contracts/lzTokenBridge/lzApp/NonblockingLzApp.sol b/packages/boba/contracts/contracts/lzTokenBridge/lzApp/NonblockingLzApp.sol new file mode 100644 index 0000000000..095e34dc69 --- /dev/null +++ b/packages/boba/contracts/contracts/lzTokenBridge/lzApp/NonblockingLzApp.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./LzApp.sol"; + +/* + * the default LayerZero messaging behaviour is blocking, i.e. any failed message will block the channel + * this abstract class try-catch all fail messages and store locally for future retry. hence, non-blocking + * NOTE: if the srcAddress is not configured properly, it will still block the message pathway from (srcChainId, srcAddress) + */ +abstract contract NonblockingLzApp is LzApp { + mapping(uint16 => mapping(bytes => mapping(uint64 => bytes32))) public failedMessages; + + event MessageFailed(uint16 _srcChainId, bytes _srcAddress, uint64 _nonce, bytes _payload); + + function __NonblockingLzApp_init(address _endpoint) internal initializer { + LzApp.__LzApp_init(_endpoint); + } + + // overriding the virtual function in LzReceiver + function _blockingLzReceive(uint16 _srcChainId, bytes memory _srcAddress, uint64 _nonce, bytes memory _payload) internal virtual override { + // try-catch all errors/exceptions + try this.nonblockingLzReceive(_srcChainId, _srcAddress, _nonce, _payload) { + // do nothing + } catch { + // error / exception + failedMessages[_srcChainId][_srcAddress][_nonce] = keccak256(_payload); + emit MessageFailed(_srcChainId, _srcAddress, _nonce, _payload); + } + } + + function nonblockingLzReceive(uint16 _srcChainId, bytes memory _srcAddress, uint64 _nonce, bytes memory _payload) public virtual { + // only internal transaction + require(_msgSender() == address(this), "NonblockingLzApp: caller must be LzApp"); + _nonblockingLzReceive(_srcChainId, _srcAddress, _nonce, _payload); + } + + //@notice override this function + function _nonblockingLzReceive(uint16 _srcChainId, bytes memory _srcAddress, uint64 _nonce, bytes memory _payload) internal virtual; + + function retryMessage(uint16 _srcChainId, bytes memory _srcAddress, uint64 _nonce, bytes memory _payload) public payable virtual { + // assert there is message to retry + bytes32 payloadHash = failedMessages[_srcChainId][_srcAddress][_nonce]; + require(payloadHash != bytes32(0), "NonblockingLzApp: no stored message"); + require(keccak256(_payload) == payloadHash, "NonblockingLzApp: invalid payload"); + // clear the stored message + failedMessages[_srcChainId][_srcAddress][_nonce] = bytes32(0); + // execute the message. revert if it fails again + _nonblockingLzReceive(_srcChainId, _srcAddress, _nonce, _payload); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} diff --git a/packages/boba/gateway/.env.example b/packages/boba/gateway/.env.example index 4839b98dc6..f81434f3c3 100644 --- a/packages/boba/gateway/.env.example +++ b/packages/boba/gateway/.env.example @@ -1,6 +1,7 @@ REACT_APP_INFURA_ID= REACT_APP_ETHERSCAN_API= REACT_APP_POLL_INTERVAL=15000 +REACT_APP_GAS_POLL_INTERVAL=30000 SKIP_PREFLIGHT_CHECK=true REACT_APP_WALLET_VERSION=1.0.10 REACT_APP_ENV=dev diff --git a/packages/boba/gateway/readme.md b/packages/boba/gateway/readme.md index a50d32f971..0f918bd848 100644 --- a/packages/boba/gateway/readme.md +++ b/packages/boba/gateway/readme.md @@ -18,3 +18,4 @@ gateway.boba.betwork. | REACT_APP_GA4_MEASUREMENT_ID | Yes | N/A | Google analytics api key | | REACT_APP_SENTRY_DSN | Yes | N/A | Sentry DSN url to catch the error on frontend | | REACT_APP_ENABLE_LOCK_PAGE | No | N/A | to enable the lock page on gateway menu | +| REACT_APP_GAS_POLL_INTERVAL | Yes | 30000 | Poll interval to fetch the gas price and verifier status | diff --git a/packages/boba/gateway/src/actions/balanceAction.js b/packages/boba/gateway/src/actions/balanceAction.js index 76a76d8522..cd0a0c04f3 100644 --- a/packages/boba/gateway/src/actions/balanceAction.js +++ b/packages/boba/gateway/src/actions/balanceAction.js @@ -62,6 +62,10 @@ export function fetchFastDepositCost(address) { return createAction('FETCH/FASTDEPOSIT/COST', () => networkService.getFastDepositCost(address)) } +export function fetchAltL1DepositFee() { + return createAction('FETCH/ALTL1DEPOSIT/COST', () => networkService.getAltL1DepositFee()) +} + export function fetchFastDepositBatchCost(tokenList) { return createAction('FETCH/FASTDEPOSIT/BATCH/COST', () => networkService.getFastDepositBatchCost(tokenList)) } diff --git a/packages/boba/gateway/src/actions/createAction.js b/packages/boba/gateway/src/actions/createAction.js index e326555301..de69371b6a 100644 --- a/packages/boba/gateway/src/actions/createAction.js +++ b/packages/boba/gateway/src/actions/createAction.js @@ -46,9 +46,6 @@ export function createAction (key, asyncAction) { //deal with metamask errors - they will have a 'code' field so we can detect those if(response && response.hasOwnProperty('message') && response.hasOwnProperty('code')) { - - console.log("Error keys:", Object.keys(response)) - console.log("Error code:", response.code) Sentry.captureMessage(response.reason) if(response.hasOwnProperty('reason')) console.log("Error reason:", response.reason) @@ -95,7 +92,6 @@ export function createAction (key, asyncAction) { return true } catch (error) { - console.log("Unhandled error RAW:", {error, key, asyncAction}) Sentry.captureException(error); return false diff --git a/packages/boba/gateway/src/actions/networkAction.js b/packages/boba/gateway/src/actions/networkAction.js index c0b868dc68..e73828ca5f 100644 --- a/packages/boba/gateway/src/actions/networkAction.js +++ b/packages/boba/gateway/src/actions/networkAction.js @@ -100,6 +100,11 @@ export function depositErc20(value, currency, currencyL2) { ) } +//DEPOSIT ERC20 to Alt L1 bridge +export function depositErc20ToL1(payload) { + return createAction('DEPOSIT_ALTL1/CREATE', () => networkService.depositErc20ToL1(payload)) +} + //FARM export function farmL1(value_Wei_String, currencyAddress) { return createAction('FARM/CREATE', () => @@ -118,7 +123,7 @@ export function getReward(currencyAddress, value_Wei_String, L1orL2Pool) { } export function withdrawLiquidity(currencyAddress, value_Wei_String, L1orL2Pool) { - console.log("Withdrawing ERC20 Liquidity") + return createAction('FARM/WITHDRAW', () => networkService.withdrawLiquidity(currencyAddress, value_Wei_String, L1orL2Pool) ) diff --git a/packages/boba/gateway/src/actions/setupAction.js b/packages/boba/gateway/src/actions/setupAction.js index 1595daf704..4ec75974dc 100644 --- a/packages/boba/gateway/src/actions/setupAction.js +++ b/packages/boba/gateway/src/actions/setupAction.js @@ -25,7 +25,6 @@ export function setEnableAccount(enabled) { } export function setBaseState(enabled) { - console.log("setBaseState:", enabled) return function (dispatch) { return dispatch({ type: 'SETUP/BASE/SET', payload: enabled }) } @@ -38,7 +37,6 @@ export function setNetwork(network) { } export function setLayer(layer) { - console.log("SA: Setting layer to:", layer) return function (dispatch) { return dispatch({ type: 'SETUP/LAYER/SET', payload: layer }) } @@ -51,12 +49,10 @@ export function setWalletAddress(account) { } export function switchChain(layer) { - console.log("SA: Switching chain to", layer) return createAction('SETUP/SWITCH', () => networkService.switchChain(layer)) } export function switchFee(targetFee) { - console.log("SA: Switching fee to", targetFee) return createAction('SETUP/SWITCHFEE', () => networkService.switchFee(targetFee)) } diff --git a/packages/boba/gateway/src/actions/tokenAction.js b/packages/boba/gateway/src/actions/tokenAction.js index d50f6b0322..e3627aec26 100644 --- a/packages/boba/gateway/src/actions/tokenAction.js +++ b/packages/boba/gateway/src/actions/tokenAction.js @@ -158,11 +158,26 @@ export async function addToken ( tokenContractAddressL1 ) { ) } - const [ _symbolL1, _decimals, _name ] = await Promise.all([ - tokenContract.symbol(), - tokenContract.decimals(), - tokenContract.name() - ]).catch(e => [ null, null, null ]) + let _symbolL1 + let _decimals + let _name + + if (ethers.utils.isAddress(_tokenContractAddressL1)) { + const tokenInfo = networkService.tokenInfo.L1[ethers.utils.getAddress(_tokenContractAddressL1)] + if (tokenInfo) { + _symbolL1 = tokenInfo.symbol + _decimals = tokenInfo.decimals + _name = tokenInfo.name + } + } + + if (!_symbolL1 || !_decimals || !_name) { + [ _symbolL1, _decimals, _name ] = await Promise.all([ + tokenContract.symbol(), + tokenContract.decimals(), + tokenContract.name() + ]).catch(e => [ null, null, null ]) + } const decimals = _decimals ? Number(_decimals.toString()) : 'NOT ON ETHEREUM' const symbolL1 = _symbolL1 || 'NOT ON ETHEREUM' diff --git a/packages/boba/gateway/src/actions/uiAction.js b/packages/boba/gateway/src/actions/uiAction.js index 92b46d3bb7..7cdf77c152 100644 --- a/packages/boba/gateway/src/actions/uiAction.js +++ b/packages/boba/gateway/src/actions/uiAction.js @@ -19,9 +19,9 @@ export function setTheme (theme) { } } -export function openModal (modal, token, fast, tokenIndex, lock) { +export function openModal (modal, token, fast, tokenIndex, lock, proposalId) { return function (dispatch) { - return dispatch({ type: 'UI/MODAL/OPEN', payload: modal, token, fast, tokenIndex, lock }); + return dispatch({ type: 'UI/MODAL/OPEN', payload: modal, token, fast, tokenIndex, lock, proposalId }); } } diff --git a/packages/boba/gateway/src/actions/veBobaAction.js b/packages/boba/gateway/src/actions/veBobaAction.js index 70234a1219..481ae7f542 100644 --- a/packages/boba/gateway/src/actions/veBobaAction.js +++ b/packages/boba/gateway/src/actions/veBobaAction.js @@ -6,16 +6,6 @@ import { createAction } from "./createAction"; **** VE Boba Actions *** ************************/ -/** - * @VeBobaAction actions can have. - * - number of locks - * - createLock - * - increaseLockAmount - * - extendLockTime - * - withdrawLock - * - fetchLockRecords - */ - export function createLock(payload) { return createAction('LOCK/CREATE', () => networkService.createLock(payload)) } @@ -35,3 +25,15 @@ export function extendLockTime(payload) { export function fetchLockRecords() { return createAction('LOCK/RECORDS', () => networkService.fetchLockRecords()) } + +export function fetchPools() { + return createAction('VOTE/POOLS', () => networkService.fetchPools()) +} + +export function onSavePoolVote(payload) { + return createAction('SAVE_POOL/VOTE', () => networkService.savePoolVote(payload)) +} + +export function onDistributePool(payload) { + return createAction('DISTRIBUTE/POOL', () => networkService.distributePool(payload)) +} diff --git a/packages/boba/gateway/src/components/input/Input.js b/packages/boba/gateway/src/components/input/Input.js index f7a1818963..c6d8ebb992 100644 --- a/packages/boba/gateway/src/components/input/Input.js +++ b/packages/boba/gateway/src/components/input/Input.js @@ -59,7 +59,6 @@ function Input({ async function handlePaste() { try { const text = await navigator.clipboard.readText() - console.log("copy:",text) if (text) { onChange({ target: { value: text } }) } diff --git a/packages/boba/gateway/src/components/listProposal/listProposal.js b/packages/boba/gateway/src/components/listProposal/listProposal.js index 91839d3f9c..d7ada031da 100644 --- a/packages/boba/gateway/src/components/listProposal/listProposal.js +++ b/packages/boba/gateway/src/components/listProposal/listProposal.js @@ -16,8 +16,8 @@ limitations under the License. */ import { Circle } from '@mui/icons-material' import { Box, LinearProgress, Link, Typography } from '@mui/material' import { makeStyles } from '@mui/styles' -import { castProposalVote, executeProposal, queueProposal } from 'actions/daoAction' -import { openAlert } from 'actions/uiAction' +import { executeProposal, queueProposal } from 'actions/daoAction' +import { openAlert, openModal } from 'actions/uiAction' import Button from 'components/button/Button' import moment from 'moment' import React, { useEffect, useState } from 'react' @@ -54,9 +54,8 @@ function ListProposal({ }, [ proposal ]) - const updateVote = async (id, userVote, label) => { - let res = await dispatch(castProposalVote({ id, userVote })); - if (res) dispatch(openAlert(`${label}`)) + const onVote = (id) => { + dispatch(openModal('castVoteModal', null, null, null, null, id)) } const doQueueProposal = async () => { @@ -74,13 +73,13 @@ function ListProposal({ let descList = description.split('@@') if (descList[ 1 ] !== '') { //should validate http link - return <>{descList[ 0 ]}  MORE DETAILS + return  {descList[ 0 ]}  More details } else { return <>{descList[ 0 ]} } @@ -91,8 +90,7 @@ function ListProposal({ const startTime = moment.unix(proposal.startTimestamp).format('lll') const endTime = moment.unix(proposal.endTimestamp).format('lll') - let hasVoted = false - if(proposal.hasVoted && proposal.hasVoted.hasVoted) hasVoted = true + let hasVoted = proposal.hasVoted return ( @@ -106,26 +104,30 @@ function ListProposal({ xs={12} md={12} > - - Proposal {proposal.id} : - + + Proposal {proposal.id} : + - - Voting Time - - - - {startTime} - -   -   - - {endTime} - - + + Voting Time + + + + {startTime} + +   -   + + {endTime} + + + + + + - Status:   + {/* Status:   */} {proposal.state === 'Defeated' && proposal.totalVotes < 1000000 &&   @@ -220,22 +222,8 @@ function ListProposal({ - } - {proposal.state === 'Active' && !hasVoted && - - } - {proposal.state === 'Active' && !hasVoted && - + onClick={(e) => { onVote(proposal.id) }} + >Vote } {proposal.state === 'Queued' && diff --git a/packages/boba/gateway/src/components/listProposal/listProposal.styles.js b/packages/boba/gateway/src/components/listProposal/listProposal.styles.js index 31def51947..0a2aa7204c 100644 --- a/packages/boba/gateway/src/components/listProposal/listProposal.styles.js +++ b/packages/boba/gateway/src/components/listProposal/listProposal.styles.js @@ -4,10 +4,10 @@ import { Box, Grid } from '@mui/material' export const Wrapper = styled(Box)(({ theme, ...props }) => ({ borderBottom: theme.palette.mode === 'light' ? '1px solid #c3c5c7' : '1px solid #192537', borderRadius: '12px', - background: theme.palette.background.secondary, - marginBottom: '20px', + background: theme.palette.background.default, + marginBottom: '10px', [theme.breakpoints.down('md')]: { - padding: '30px 10px', + padding: '20px 10px', }, [theme.breakpoints.up('md')]: { padding: '20px', @@ -22,9 +22,9 @@ export const GridContainer = styled(Grid)(({theme})=>({ export const GridItemTag = styled(Grid)(({ theme, ...props }) => ({ display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - alignItems:'flex-start', + flexDirection: 'column', + justifyContent: 'center', + alignItems:'flex-start', paddingLeft: '8px', [theme.breakpoints.down('md')]:{ padding: `${props.xs === 12 ? '20px 0px 0px': 'inherit'}` @@ -33,15 +33,15 @@ export const GridItemTag = styled(Grid)(({ theme, ...props }) => ({ export const GridItemTagR = styled(Grid)(({ theme, ...props }) => ({ display: 'flex', - flexDirection: 'column', - justifyContent: 'space-between', - alignItems: 'flex-start', + flexDirection: 'column', + justifyContent: 'space-between', + alignItems: 'flex-start', //paddingLeft: '8px', [theme.breakpoints.down('md')]:{ //padding: `${props.xs === 12 ? '20px 0px 0px': 'inherit'}`, flexDirection: 'row', justifyContent: 'space-between', - alignItems: 'center', + alignItems: 'center', } })) diff --git a/packages/boba/gateway/src/components/listToken/listToken.js b/packages/boba/gateway/src/components/listToken/listToken.js index 3d6ea28e8c..7691e7b4fc 100644 --- a/packages/boba/gateway/src/components/listToken/listToken.js +++ b/packages/boba/gateway/src/components/listToken/listToken.js @@ -18,6 +18,7 @@ import { selectLookupPrice } from 'selectors/lookupSelector' import { amountToUsd, logAmount } from 'util/amountConvert' import { getCoinImage } from 'util/coinImage' import * as S from './listToken.styles' +import { BRIDGE_TYPE } from 'util/constant' function ListToken({ token, @@ -125,7 +126,7 @@ function ListToken({ {enabled && chain === 'L1' && <> - + {token.symbol === 'BOBA' && + - + } + } {enabled && chain === 'L2' && @@ -368,13 +380,14 @@ function ListToken({ {enabled && chain === 'L1' && <> + {token.symbol === 'BOBA' && + + + } } {enabled && chain === 'L2' && diff --git a/packages/boba/gateway/src/components/mainMenu/gasSwitcher/GasSwitcher.js b/packages/boba/gateway/src/components/mainMenu/gasSwitcher/GasSwitcher.js index e44b402f90..81fea89790 100644 --- a/packages/boba/gateway/src/components/mainMenu/gasSwitcher/GasSwitcher.js +++ b/packages/boba/gateway/src/components/mainMenu/gasSwitcher/GasSwitcher.js @@ -1,19 +1,38 @@ import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' -import { useSelector } from 'react-redux' import * as S from './GasSwitcher.styles.js' import { selectGas } from 'selectors/balanceSelector' import { selectVerifierStatus } from 'selectors/verifierSelector' +import { selectBaseEnabled } from 'selectors/setupSelector.js' + +import { fetchGas } from 'actions/networkAction.js' +import { fetchVerifierStatus } from 'actions/verifierAction.js' import networkService from 'services/networkService.js' -function GasSwitcher({ isMobile }) { +import useInterval from 'hooks/useInterval.js' + +import { GAS_POLL_INTERVAL } from 'util/constant.js' +function GasSwitcher() { + const dispatch = useDispatch() + + const baseEnabled = useSelector(selectBaseEnabled()) const gas = useSelector(selectGas) + const verifierStatus = useSelector(selectVerifierStatus) + const [ savings, setSavings ] = useState(0) + useInterval(() => { + if (baseEnabled) { + dispatch(fetchGas()) + dispatch(fetchVerifierStatus()) + } + }, GAS_POLL_INTERVAL) + useEffect(() => { async function getGasSavings() { if (networkService.networkGateway === 'mainnet') { @@ -31,7 +50,6 @@ function GasSwitcher({ isMobile }) { getGasSavings() }, [ gas ]) - const verifierStatus = useSelector(selectVerifierStatus) return ( diff --git a/packages/boba/gateway/src/components/mainMenu/menuItems.js b/packages/boba/gateway/src/components/mainMenu/menuItems.js index d836ed1750..0e478d2874 100644 --- a/packages/boba/gateway/src/components/mainMenu/menuItems.js +++ b/packages/boba/gateway/src/components/mainMenu/menuItems.js @@ -1,33 +1,41 @@ +import { ROUTES_PATH } from "util/constant"; + export const menuItems = [ { key: 'Bridge', icon: "WalletIcon", title: "Bridge", - url: "/bridge" + url: ROUTES_PATH.BRIDGE }, { key: 'Ecosystem', icon: "SafeIcon", title: "Ecosystem", - url: "/ecosystem" + url: ROUTES_PATH.ECOSYSTEM }, { key: 'Wallet', icon: "WalletIcon", title: "Wallet", - url: "/wallet" + url: ROUTES_PATH.WALLET }, { key: 'History', icon: "HistoryIcon", title: "History", - url: "/history" + url: ROUTES_PATH.HISTORY }, { - key: 'Farm', + key: 'Earn', icon: "EarnIcon", title: "Earn", - url: "/farm", + url: ROUTES_PATH.EARN + }, + { + key: 'Stake', + icon: "StakeIcon", + title: "Stake", + url: ROUTES_PATH.STAKE }, { key: 'Lock', @@ -36,15 +44,15 @@ export const menuItems = [ url: "/lock", }, { - key: 'Save', - icon: "SaveIcon", - title: "Stake", - url: "/save", + key: 'Vote', + icon: "VoteIcon", + title: "Vote&Dao", + url: ROUTES_PATH.VOTE_DAO }, { - key: 'DAO', - icon: "DAOIcon", - title: "DAO", - url: "/dao" + key: 'LinksToBobaChains', + icon: "LinksToBobaChainsIcon", + title: "BOBA Chains", + url: ROUTES_PATH.BOBA_CHAINS } ] diff --git a/packages/boba/gateway/src/components/mainMenu/menuItems/MenuItems.js b/packages/boba/gateway/src/components/mainMenu/menuItems/MenuItems.js index bea637979a..532593c79c 100644 --- a/packages/boba/gateway/src/components/mainMenu/menuItems/MenuItems.js +++ b/packages/boba/gateway/src/components/mainMenu/menuItems/MenuItems.js @@ -7,7 +7,6 @@ import { selectMonster } from 'selectors/setupSelector' import { menuItems } from '../menuItems' import * as S from './MenuItems.styles' -import { ENABLE_LOCK_PAGE } from 'util/constant' const MenuItems = () => { @@ -37,10 +36,6 @@ const MenuItems = () => { return ( {menuList.map((item) => { - if (!+ENABLE_LOCK_PAGE && item.key === 'Lock') { - return null; - } - return ( { diff --git a/packages/boba/gateway/src/components/pageFooter/PageFooter.js b/packages/boba/gateway/src/components/pageFooter/PageFooter.js index 18fae10724..62a147bd3d 100644 --- a/packages/boba/gateway/src/components/pageFooter/PageFooter.js +++ b/packages/boba/gateway/src/components/pageFooter/PageFooter.js @@ -6,6 +6,7 @@ import BobaLogo from '../../images/boba2/logo-boba2.svg' import GasSwitcher from '../mainMenu/gasSwitcher/GasSwitcher' import * as S from './PageFooter.styles' import { useMediaQuery, useTheme } from '@mui/material' +import { ROUTES_PATH } from 'util/constant' const PageFooter = ({maintenance}) => { @@ -63,13 +64,13 @@ const PageFooter = ({maintenance}) => { {!isMobile && } FAQs AirDrop BobaScope
{label}
@@ -53,33 +58,6 @@ function Pager ({ currentPage, totalPages, isLastPage, onClickNext, onClickBack, ) - - // return ( - //
- //
{label}
- //
- //
{`Page ${currentPage} of ${totalPages}`}
- //
- // - //
- //
- // - //
- //
- //
- // ); } export default Pager; diff --git a/packages/boba/gateway/src/components/select/Custom.select.js b/packages/boba/gateway/src/components/select/Custom.select.js new file mode 100644 index 0000000000..b2ff54e47a --- /dev/null +++ b/packages/boba/gateway/src/components/select/Custom.select.js @@ -0,0 +1,50 @@ +import React from 'react' +import { Box, Typography } from '@mui/material'; +import { components } from 'react-select'; +import * as G from 'containers/Global.styles'; +import * as S from './Select.style'; + +export const Option = (props) => { + + const { + icon, + title, + label, + subTitle + } = props.data; + + return <> + + + {icon && + + {title} + + } + + {label && {label}} + {title && {title}} + {subTitle && {subTitle}} + + + + +} + + +export const MultiValue = (props) => { + + return <> + + {props.data.label} + + +} +export const SingleValue = (props) => { + + return <> + + {props.data.label} + + +} diff --git a/packages/boba/gateway/src/components/select/Select.js b/packages/boba/gateway/src/components/select/Select.js index 8bb0775de8..879e906850 100644 --- a/packages/boba/gateway/src/components/select/Select.js +++ b/packages/boba/gateway/src/components/select/Select.js @@ -14,33 +14,52 @@ See the License for the specific language governing permissions and limitations under the License. */ import React from 'react'; -import { Select as MuiSelect, MenuItem, useTheme, Typography } from '@mui/material'; +import ReactSelect from 'react-select'; +import { Select as MuiSelect, MenuItem, useTheme, Typography, Box } from '@mui/material'; import * as styles from './Select.module.scss'; import * as S from './Select.style'; import { ArrowDropDownOutlined } from '@mui/icons-material'; +import { + Option, + MultiValue, + SingleValue +} from './Custom.select'; -function Select ({ +function Select({ label, value, options, onSelect, loading, error = '', - className + className, + newSelect = false, + isMulti, + isLoading = false, }) { const theme = useTheme(); const selected = options.find(i => i.value === value); - function renderOption (i) { + function renderOption(i) { + let title = ''; if (i.title && i.subTitle) { - return `${i.title} - ${i.subTitle}`; + title = `${i.title} - ${i.subTitle}`; } if (i.title && !i.subTitle) { - return i.title; + title = i.title; } if (i.subTitle && !i.title) { - return i.subTitle; + title = i.subTitle; } + + return ( + <> + {i.image ? i.image : null} + + {title} + + + ) } const renderLoading = ( @@ -52,14 +71,14 @@ function Select ({ const renderSelect = ( <> } + IconComponent={() => } className={styles.select} value={value} onChange={onSelect} autoWidth MenuProps={{ sx: { - '&& .Mui-selected':{ + '&& .Mui-selected': { backgroundColor: 'transparent !important', color: '#BAE21A' } @@ -77,10 +96,10 @@ function Select ({ {i.title}
- {i.description} + {i.description} - : renderOption(i)} + : renderOption(i)} ))} @@ -98,6 +117,64 @@ function Select ({ ); + // TODO: Make use of react-select across all. + if (newSelect) { + return + {label && {label}} + ({ + ...base, + background: theme.palette.background.default, + borderRadius: theme.palette.primary.borderRadius, + padding: '5px 10px', + width: '100%', + border: '1px solid rgba(255, 255, 255, 0.14)' + }), + indicatorSeparator: (base) => ({ + ...base, + display: 'none' + }), + container: (base) => ({ + ...base, + background: theme.palette.background.default + }), + singleValue: (base) => ({ + ...base, + background: 'transperant', + color: theme.palette.mode === 'light' ? theme.palette.background.default : '#fff', + padding: '5px' + }), + multiValue: (base) => ({ + ...base, + background: theme.palette.background.secondary, + color: theme.palette.mode === 'light' ? theme.palette.background.default : '#fff', + marginRight: '5px', + paddingRight: '5px', + }), + valueContainer: (base) => ({ + ...base, + background: theme.palette.background.default + }) + }} + theme={theme} + components={{ + Option, + MultiValue, + SingleValue + }} + /> + + } + return (
({ alignItems: 'center', borderRadius: '12px', })) + + +export const SelectOptionContainer = styled(Box)(({ theme }) => ({ + background: theme.palette.background.secondary, + border: '1px solid rgba(255, 255, 255, 0.15)', + display: 'flex', + alignItems: 'center' +})); diff --git a/packages/boba/gateway/src/components/transaction/Transaction.js b/packages/boba/gateway/src/components/transaction/Transaction.js index 4889df6594..dc50c1d9f4 100644 --- a/packages/boba/gateway/src/components/transaction/Transaction.js +++ b/packages/boba/gateway/src/components/transaction/Transaction.js @@ -43,6 +43,9 @@ function Transaction({ oriHash, amountTx, completion = '', + tx_ref = null, + eventType, + toChain }) { const [dropDownBox, setDropDownBox] = useState(false) @@ -133,10 +136,14 @@ function Transaction({ {completion !== '' && {completion} } + {toChain && + {toChain} + } {oriChain} Hash:  {typeTX} + {eventType ? + {eventType} + : null} {amountTx ? ({ display: 'flex', justifyContent: 'space-around', - margin: '20px auto', + margin: '0 auto', width: '100%', gap: '10px', [ theme.breakpoints.down('sm') ]: { @@ -15,9 +15,9 @@ export const Container = styled(Box)(({ theme }) => ({ }, })) -export const ContentEmpty = styled(Box)(({ theme }) => ({ +export const ContentEmpty = styled(Box)(({ theme, minHeight, p }) => ({ width: '100%', - minHeight: '400px', + minHeight: minHeight || '400px', display: 'flex', justifyContent: 'center', alignItems: 'center', @@ -124,3 +124,62 @@ export const PageSwitcher = styled(Box)(({ theme }) => ({ }, })); + + +export const ThumbnailContainer = styled(Box)(({ theme }) => ({ + background: theme.palette.background.secondary, + borderRadius: theme.palette.primary.borderRadius, + border: '1px solid rgba(255, 255, 255, 0.15)', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '3rem', + width: '3rem', +})) + + +// Table Styled Component +export const TableHeading = styled(Box)(({ theme }) => ({ + width: '100%', + padding: "10px", + display: "flex", + alignItems: "center", + flexDirection: 'row', + justifyContent: "space-between", + borderBottom: theme.palette.primary.borderBottom, + [ theme.breakpoints.down('md') ]: { + justifyContent: 'flex-start', + marginBottom: "5px", + 'div:last-child': { + display: 'none' + } + }, +})); + +export const TableHeadingItem = styled(Typography)` + width: 20%; + gap: 5px; +`; + +export const TableBody = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + textAlign: 'center', + width: '100%', + [ theme.breakpoints.down('sm') ]: { + gap: '10px' + } +})) + +export const TableCell = styled(Box)(({ theme, isMobile, width, flex }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-start', + width: width || '20%', + flex: flex || 1, + [ theme.breakpoints.down('sm') ]: { + minWidth: '20%', + width: isMobile ? '10%' : 'unset' + } +})); diff --git a/packages/boba/gateway/src/containers/VoteAndDao/Dao/Dao.js b/packages/boba/gateway/src/containers/VoteAndDao/Dao/Dao.js new file mode 100644 index 0000000000..e2c6b5d8ef --- /dev/null +++ b/packages/boba/gateway/src/containers/VoteAndDao/Dao/Dao.js @@ -0,0 +1,167 @@ +/* +Copyright 2021-present Boba Network. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { Box, Typography } from '@mui/material' +import { openError, openModal } from 'actions/uiAction' +import { orderBy } from 'lodash' + +import Button from 'components/button/Button' +import ListProposal from 'components/listProposal/listProposal' + +import Select from 'components/select/Select' + +import { selectLatestProposalState, selectProposals } from 'selectors/daoSelector' +import { selectLoading } from 'selectors/loadingSelector' +import { selectAccountEnabled, selectLayer } from 'selectors/setupSelector' + +import { fetchLockRecords } from 'actions/veBobaAction' +import { selectLockRecords } from 'selectors/veBobaSelector' + + +import {DividerLine} from 'containers/Global.styles' +import * as S from './Dao.styles' + +const PROPOSAL_STATES = [ + { value: 'All', label: 'All' }, + { value: 'Pending', label: 'Pending' }, + { value: 'Active', label: 'Active' }, + { value: 'Canceled', label: 'Canceled' }, + { value: 'Defeated', label: 'Defeated' }, + { value: 'Succeeded', label: 'Succeeded' }, + { value: 'Queued', label: 'Queued' }, + { value: 'Expired', label: 'Expired' }, + { value: 'Executed', label: 'Executed' } +] + +function DAO({ + connectToBOBA +}) { + + const dispatch = useDispatch() + + const nftRecords = useSelector(selectLockRecords); + const accountEnabled = useSelector(selectAccountEnabled()) + const layer = useSelector(selectLayer()); + const loading = useSelector(selectLoading([ 'PROPOSALS/GET' ])) + const hasLiveProposal = useSelector(selectLatestProposalState) + + let proposals = useSelector(selectProposals) + proposals = orderBy(proposals, i => i.startTimestamp, 'desc') + + const [ balance, setBalance ] = useState('--'); + const [ selectedState, setSelectedState ] = useState(PROPOSAL_STATES[ 0 ]) + + useEffect(() => { + if (!!accountEnabled) { + dispatch(fetchLockRecords()); + } + }, [ accountEnabled, dispatch ]); + + useEffect(() => { + if (!!accountEnabled) { + const veBoba = nftRecords.reduce((s, record) => s + Number(record.balance), 0); + setBalance(veBoba.toFixed(2)) + } + }, [ accountEnabled, nftRecords ]); + + + return ( + + + + Voting power + govBOBA: + {balance} + + + { + (!accountEnabled || layer !== 'L2' )? + + : + } + + {accountEnabled + && nftRecords + && !nftRecords.length + ? + Oh! You don't have veBoba NFT, Please go to Lock to get them. + + : null + } + + + + Proposals + + + + + {!!loading && !proposals.length ? Loading... : null} + {proposals + // eslint-disable-next-line array-callback-return + .filter((p) => { + if (selectedState.value === 'All') { + return true; + } + return selectedState.value === p.state; + }) + .map((p, index) => { + return + + + })} + + + + ) +} + +export default React.memo(DAO) diff --git a/packages/boba/gateway/src/containers/dao/Dao.styles.js b/packages/boba/gateway/src/containers/VoteAndDao/Dao/Dao.styles.js similarity index 92% rename from packages/boba/gateway/src/containers/dao/Dao.styles.js rename to packages/boba/gateway/src/containers/VoteAndDao/Dao/Dao.styles.js index 4b620d489d..dfd63b26f9 100644 --- a/packages/boba/gateway/src/containers/dao/Dao.styles.js +++ b/packages/boba/gateway/src/containers/VoteAndDao/Dao/Dao.styles.js @@ -9,6 +9,7 @@ export const DaoPageContainer = styled(Box)(({ theme }) => ({ padding: '10px', paddingTop: '0px', width: '70%', + gap: '10px', [theme.breakpoints.between('md', 'lg')]: { width: '90%', padding: '0px', @@ -42,6 +43,7 @@ export const DaoWalletContainer = styled(Box)(({ theme }) => ({ padding: '0px 20px', minHeight: '700px', width: '30%', + gap: '10px', borderRadius: theme.palette.primary.borderRadius, background: theme.palette.background.secondary, [theme.breakpoints.down('sm')]: { @@ -49,14 +51,6 @@ export const DaoWalletContainer = styled(Box)(({ theme }) => ({ }, })); -export const DaoWalletAction = styled(Box)(({ theme }) => ({ - display: 'flex', - justifyContent: 'space-around', - width: '100%', - margin: '10px auto', - gap: '10px', -})); - export const DaoProposalContainer = styled(Box)(({ theme }) => ({ width: '70%', display: 'flex', @@ -77,7 +71,7 @@ export const DaoProposalHead = styled(Box)(({ theme }) => ({ alignItems: 'center', alignSelf: 'flex-start', justifyContent: 'space-between', - padding: '24px 0px', + padding: '15px 0px', width: '100%', margin: '5px', [theme.breakpoints.down('sm')]: { diff --git a/packages/boba/gateway/src/containers/VoteAndDao/Vote/Pools/poolList.js b/packages/boba/gateway/src/containers/VoteAndDao/Vote/Pools/poolList.js new file mode 100644 index 0000000000..e7978e82a5 --- /dev/null +++ b/packages/boba/gateway/src/containers/VoteAndDao/Vote/Pools/poolList.js @@ -0,0 +1,84 @@ +/* +Copyright 2021-present Boba Network. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +import React, { useState } from 'react' + +import { poolsTableHeads } from './pools.tableHeads' + +import * as G from 'containers/Global.styles' +import PoolListItem from './poolListItem' +import Pager from 'components/pager/Pager' +import { PER_PAGE } from 'util/constant' +import { useSelector } from 'react-redux' +import { selectPools } from 'selectors/veBobaSelector' +import { selectLoading } from 'selectors/loadingSelector' +import { Typography } from '@mui/material' + + +function PoolList({ onPoolVoteChange, token, onDistribute }) { + + const [ page, setPage ] = useState(1) + + const loading = useSelector(selectLoading([ 'VOTE/POOLS' ])) + const pools = useSelector(selectPools) + + // pagination logic + const startingIndex = page === 1 ? 0 : ((page - 1) * PER_PAGE) + const endingIndex = page * PER_PAGE + const paginatedPools = pools.slice(startingIndex, endingIndex) + let totalNumberOfPages = Math.ceil(pools.length / PER_PAGE) + if (totalNumberOfPages === 0) totalNumberOfPages = 1 + + return + + { + poolsTableHeads.map((item) => { + return ( + {item.label} + + ) + }) + } + + {loading && Loading...} + {paginatedPools.map((pool) => { + return + + + })} + setPage(page + 1)} + onClickBack={() => setPage(page - 1)} + /> + +} + +export default PoolList; diff --git a/packages/boba/gateway/src/containers/VoteAndDao/Vote/Pools/poolListItem.js b/packages/boba/gateway/src/containers/VoteAndDao/Vote/Pools/poolListItem.js new file mode 100644 index 0000000000..7fa0438fd0 --- /dev/null +++ b/packages/boba/gateway/src/containers/VoteAndDao/Vote/Pools/poolListItem.js @@ -0,0 +1,146 @@ +/* +Copyright 2021-present Boba Network. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +import React, { useEffect, useState } from 'react' +import { Box, Typography, Slider, styled } from '@mui/material' + +import Button from 'components/button/Button' +import bobaLogo from 'images/boba-token.svg' + +import * as G from 'containers/Global.styles' + +// styled component +const ListItemContent = styled(Box)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: '20px', + width: '100%', + padding: '10px', + borderBottom: theme.palette.primary.borderBottom +})) + + +function PoolListItem({ + pool, + onPoolVoteChange, + token, + onDistribute, +}) { + + const [ selectedVote, setSelectedVote ] = useState(0); + const [ myVote, setMyVote ] = useState({}); + + const handleVoteChange = (e, value) => { + setSelectedVote(value); + onPoolVoteChange(pool.poolId, value); + } + + useEffect(() => { + if (token) { + let tokenUsed = pool.usedTokens.find((t) => t.tokenId === token.tokenId); + if (tokenUsed) { + let tokenBalance = Number(token.balance); + let poolVote = Number(tokenUsed.vote); + let votePercent = Number((poolVote / tokenBalance) * 100); + setMyVote({ + value: poolVote.toFixed(2), + votePercent, + }) + setSelectedVote(votePercent) + } else { + setSelectedVote(0) + setMyVote({}) + } + } + + }, [ token, pool ]); + + return + + + boba logo + + + {pool.name} + + + {pool.description} + + + + + + + {pool.totalVotes} + + + {pool.votePercentage.toFixed(2)}% + + + + + + + {myVote.value || 0} + + + {myVote.votePercent || 0}% + + + + + + + {selectedVote.toFixed(2)}% + + + + + + + + + + + +} + +export default PoolListItem; diff --git a/packages/boba/gateway/src/containers/VoteAndDao/Vote/Pools/pools.tableHeads.js b/packages/boba/gateway/src/containers/VoteAndDao/Vote/Pools/pools.tableHeads.js new file mode 100644 index 0000000000..7d4d1d7eb4 --- /dev/null +++ b/packages/boba/gateway/src/containers/VoteAndDao/Vote/Pools/pools.tableHeads.js @@ -0,0 +1,39 @@ +export const poolsTableHeads = [ + { + label: 'WAGMI Pools', + isSort: false, + size: '20%', + flex: 1, + }, + { + label: 'Total Votes', + isSort: false, + size: '20%', + flex: 1, + }, + { + label: 'My Votes', + isSort: false, + size: '20%', + flex: 1, + }, + { + label: 'My Vote%', + isSort: false, + size: '30%', + flex: 2, + sx: { + textAlign: 'center' + } + } + , + { + label: 'Action', + isSort: false, + size: '10%', + flex: 1, + sx: { + textAlign: 'center' + } + } +] diff --git a/packages/boba/gateway/src/containers/VoteAndDao/Vote/VeNfts/VeNfts.list.js b/packages/boba/gateway/src/containers/VoteAndDao/Vote/VeNfts/VeNfts.list.js new file mode 100644 index 0000000000..b399b6eb84 --- /dev/null +++ b/packages/boba/gateway/src/containers/VoteAndDao/Vote/VeNfts/VeNfts.list.js @@ -0,0 +1,79 @@ +import React from 'react' +import { Box, styled, Typography } from '@mui/material' +import CheckMarkIcon from '@mui/icons-material/CheckCircleOutline' + +import Carousel from 'react-multi-carousel' +import "react-multi-carousel/lib/styles.css"; + +import BobaNFTGlass from 'images/boba2/BobaNFTGlass.svg' + +import * as G from 'containers/Global.styles' + +const NftContainer = styled(Box)(({ theme, active }) => ({ + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + background: active ? theme.palette.background.secondary : theme.palette.background.default, + borderRadius: theme.palette.primary.borderRadius, + border: theme.palette.primary.border, + cursor: 'pointer' +})) + +const responsive = { + superLargeDesktop: { + // the naming can be any, depends on you. + breakpoint: { max: 4000, min: 3000 }, + items: 5 + }, + desktop: { + breakpoint: { max: 3000, min: 1024 }, + items: 5 + }, + tablet: { + breakpoint: { max: 1024, min: 464 }, + items: 4 + }, + mobile: { + breakpoint: { max: 464, min: 0 }, + items: 2 + } +}; + + +const VeNftsList = ({ nftRecords, selectedNft, onSelectNft }) => { + + return + {nftRecords.map((nft) => { + return { onSelectNft(nft) }} + > + {nft.tokenId === selectedNft?.tokenId ? + : null} + + {nft.tokenId} + + + #{nft.tokenId} + {nft.balance.toFixed(2)} veBoba + + + })} + +} + +export default React.memo(VeNftsList) diff --git a/packages/boba/gateway/src/containers/VoteAndDao/Vote/Vote.js b/packages/boba/gateway/src/containers/VoteAndDao/Vote/Vote.js new file mode 100644 index 0000000000..f3752ac952 --- /dev/null +++ b/packages/boba/gateway/src/containers/VoteAndDao/Vote/Vote.js @@ -0,0 +1,184 @@ +/* +Copyright 2021-present Boba Network. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { Box, styled, Typography } from '@mui/material' + +import Button from 'components/button/Button' + +import { openAlert } from 'actions/uiAction' + +import { + fetchLockRecords, + fetchPools, + onDistributePool, + onSavePoolVote +} from 'actions/veBobaAction' + +import { selectAccountEnabled, selectLayer } from 'selectors/setupSelector' +import { selectLockRecords, selectPools } from 'selectors/veBobaSelector' + +import { ContentEmpty } from 'containers/Global.styles' + +import PoolList from './Pools/poolList' +import VeNftsList from './VeNfts/VeNfts.list'; + + +// styled component +const Action = styled(Box)({ + display: 'flex', + width: '100%', + justifyContent: 'flex-end' +}); + + + +function Vote({ + connectToBOBA +}) { + const dispatch = useDispatch() + + const nftRecords = useSelector(selectLockRecords); + const accountEnabled = useSelector(selectAccountEnabled()) + const layer = useSelector(selectLayer()) + const pools = useSelector(selectPools) + + const [ selectedNft, setSelectedNft ] = useState(null); + const [ poolsVote, setPoolsVote ] = useState({}); + const [ usedVotingPower, setUsedVotingPower ] = useState(0); + + const onPoolVoteChange = (poolId, value) => { + setPoolsVote({ + ...poolsVote, + [ poolId ]: value + }) + } + + const onVote = async () => { + const res = await dispatch(onSavePoolVote({ + tokenId: selectedNft.tokenId, + pools: Object.keys(poolsVote), + weights: Object.values(poolsVote), + })) + if (res) { + setSelectedNft(null) + dispatch(fetchLockRecords()); + dispatch(fetchPools()); + dispatch( + openAlert(`Vote has been submitted successfully!`) + ) + } + } + + const onDistribute = async (gaugeAddress) => { + const res = await dispatch(onDistributePool({ + gaugeAddress + })) + + if (res) { + dispatch(fetchPools()); + dispatch(fetchLockRecords()); + dispatch( + openAlert(`Pool has been distributed successfully!`) + ) + } + } + + useEffect(() => { + if (selectedNft && pools.length > 0) { + let usedVotes = {}; + pools.forEach((pool) => { + let tokenUsed = pool.usedTokens.find((t) => t.tokenId === selectedNft.tokenId) + if (tokenUsed) { + let nftBalance = parseInt(selectedNft.balance); + let poolVote = Number(tokenUsed.vote); + let votePercent = parseInt((poolVote / nftBalance) * 100); + + usedVotes = { + ...usedVotes, + [pool.poolId]: votePercent + } + } + }) + setPoolsVote(usedVotes) + } + }, [ selectedNft, pools ]); + + useEffect(() => { + if (Object.keys(poolsVote).length > 0) { + let powerSum = Object.values(poolsVote).reduce((s, a) => s + a, 0); + setUsedVotingPower(parseInt(powerSum)) + } + },[poolsVote]) + + useEffect(() => { + if (!!accountEnabled && layer === 'L2') { + dispatch(fetchLockRecords()); + dispatch(fetchPools()); + } + }, [ accountEnabled, dispatch, layer ]); + + return <> + + Please select a govBoba to vote + { + !nftRecords.length + ? + Oh! You don't have veBoba NFT, Please go to Lock to get them. + + : + } + + + {(!accountEnabled || layer !== 'L2') ? + + : + {selectedNft && Selected #{selectedNft.tokenId}, Voting power used {usedVotingPower} %} + + + } + + + +} + +export default React.memo(Vote) diff --git a/packages/boba/gateway/src/containers/VoteAndDao/index.js b/packages/boba/gateway/src/containers/VoteAndDao/index.js new file mode 100644 index 0000000000..d6b5523e97 --- /dev/null +++ b/packages/boba/gateway/src/containers/VoteAndDao/index.js @@ -0,0 +1,107 @@ +import React, { useEffect, useState } from 'react' +import { Box, Typography } from '@mui/material' + +import * as S from './Dao/Dao.styles'; +import * as G from '../Global.styles' +import PageTitle from 'components/pageTitle/PageTitle'; +import Tabs from 'components/tabs/Tabs'; +import { useDispatch, useSelector } from 'react-redux'; +import { selectLockRecords } from 'selectors/veBobaSelector'; +import { selectAccountEnabled, selectLayer } from 'selectors/setupSelector'; +import { fetchLockRecords } from 'actions/veBobaAction'; +import Vote from './Vote/Vote'; +import Dao from './Dao/Dao'; +import { setConnectBOBA } from 'actions/setupAction'; + +const DAO_TABS = { + VOTE: "Liquidity Bootstrapping", + PROPOSAL: "Proposals", +} + +function VoteAndDAO() { + + const [ page, setPage ] = useState(DAO_TABS.VOTE); + const [ balance, setBalance ] = useState('--'); + + const dispatch = useDispatch() + const nftRecords = useSelector(selectLockRecords); + const accountEnabled = useSelector(selectAccountEnabled()) + const layer = useSelector(selectLayer()) + + + async function connectToBOBA() { + dispatch(setConnectBOBA(true)) + } + + useEffect(() => { + if (!!accountEnabled && layer === 'L2') { + dispatch(fetchLockRecords()); + } + }, [ accountEnabled, dispatch, layer ]); + + useEffect(() => { + if (!!accountEnabled) { + const veBoba = nftRecords.reduce((s, record) => s + Number(record.balance), 0); + setBalance(veBoba.toFixed(2)) + } + }, [ accountEnabled, nftRecords ]); + + const veNftDisclaimer = () => { + if (page !== DAO_TABS.VOTE) { + return null; + } + + return ( + + Votes are due by Wednesday at 23:59 UTC, when the next epoch begins. Each veNFT can only cast votes once per epoch. Your vote will allocate 100% of that veNFT's vote-power. Each veNFT's votes will carry over into the next epoch. However, you must resubmit each veNFT's vote in each epoch to earn the bribes placed in that epoch. Voters will earn bribes no matter when in the epoch the bribes are added. + + + For details refer to our Docs + + ) + } + + const handlePageChange = (type) => { + if (DAO_TABS.VOTE === type) { + setPage(DAO_TABS.VOTE) + } else if (DAO_TABS.PROPOSAL === type) { + setPage(DAO_TABS.PROPOSAL) + } + } + + return + + {/* page hero section */} + + + My total voting power + {balance} + govBOBA + + {veNftDisclaimer()} + + + {/* page tabs section */} + + handlePageChange(t)} + aria-label="Page Tab" + tabs={[ DAO_TABS.VOTE, DAO_TABS.PROPOSAL ]} + /> + + {/* page content section */} + {DAO_TABS.VOTE === page ? + + : } + + + +} + + +export default React.memo(VoteAndDAO); diff --git a/packages/boba/gateway/src/containers/dao/Dao.js b/packages/boba/gateway/src/containers/dao/Dao.js deleted file mode 100644 index 5b3ad2f8fd..0000000000 --- a/packages/boba/gateway/src/containers/dao/Dao.js +++ /dev/null @@ -1,199 +0,0 @@ -/* -Copyright 2021-present Boba Network. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. */ - -import React, { useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' - -import { openError, openModal } from 'actions/uiAction' -import { Box, Typography } from '@mui/material' -import { orderBy } from 'lodash' - -import Button from 'components/button/Button' -import ListProposal from 'components/listProposal/listProposal' -import PageTitle from 'components/pageTitle/PageTitle' -import Select from 'components/select/Select' - -import Connect from 'containers/connect/Connect' - -import { selectDaoBalance, selectDaoVotes, selectDaoBalanceX, selectDaoVotesX, selectProposalThreshold } from 'selectors/daoSelector' -import { selectLayer, selectAccountEnabled } from 'selectors/setupSelector' -import { selectProposals } from 'selectors/daoSelector' -import { selectLoading } from 'selectors/loadingSelector' - -import * as S from './Dao.styles' -import * as G from 'containers/Global.styles' -import * as styles from './Dao.module.scss' - -const PER_PAGE = 8 - -function DAO() { - - const dispatch = useDispatch() - - const balance = useSelector(selectDaoBalance) - const balanceX = useSelector(selectDaoBalanceX) - const votes = useSelector(selectDaoVotes) - const votesX = useSelector(selectDaoVotesX) - const proposalThreshold = useSelector(selectProposalThreshold) - - let layer = useSelector(selectLayer()) - const accountEnabled = useSelector(selectAccountEnabled()) - - const [selectedState, setSelectedState] = useState('All') - - const loading = useSelector(selectLoading([ 'PROPOSALS/GET' ])) - const proposals = useSelector(selectProposals) - - const options = [ - {value: 'All', title: 'All'}, - {value: 'Pending', title: 'Pending'}, - {value: 'Active', title: 'Active'}, - {value: 'Canceled', title: 'Canceled'}, - {value: 'Defeated', title: 'Defeated'}, - {value: 'Succeeded', title: 'Succeeded'}, - {value: 'Queued', title: 'Queued'}, - {value: 'Expired', title: 'Expired'}, - {value: 'Executed', title: 'Executed'} - ] - - const onActionChange = (e) => { - setSelectedState(e.target.value) - } - - const orderedProposals = orderBy(proposals, i => i.startTimestamp, 'desc') - const paginatedProposals = orderedProposals - - let totalNumberOfPages = Math.ceil(orderedProposals.length / PER_PAGE) - if (totalNumberOfPages === 0) totalNumberOfPages = 1 - - return ( -
- - - - - - - - - - - Balances - BOBA: - {!!layer ? Math.round(Number(balance)) : '--'} - xBOBA: - {!!layer ? Math.round(Number(balanceX)) : '--'} - - - - Votes - Boba: - {!!layer ? Math.round(Number(votes)) : '--'} - xBoba: - {!!layer ? Math.round(Number(votesX)) : '--'} - Total: - {!!layer ? Math.round(Number(votes) + Number(votesX)) : '--'} - {layer === 'L2' && - - - - - } - - Only votes delegated BEFORE the start of the active voting period are counted in your vote - - - - - - - At least {proposalThreshold} BOBA + xBOBA are needed to create a new proposal - - - - - - Proposals - - - - - {!!loading && !proposals.length ?
Loading...
: null} - {paginatedProposals - // eslint-disable-next-line array-callback-return - .filter((p) => { - if (selectedState === 'All') { - return true; - } - return selectedState === p.state; - }) - .map((p, index) => { - return - - - })} -
-
-
-
-
- ) -} - -export default React.memo(DAO) diff --git a/packages/boba/gateway/src/containers/dao/Dao.module.scss b/packages/boba/gateway/src/containers/dao/Dao.module.scss deleted file mode 100644 index 2df1d1858c..0000000000 --- a/packages/boba/gateway/src/containers/dao/Dao.module.scss +++ /dev/null @@ -1,84 +0,0 @@ -@import 'index.scss'; - -.container { - display: flex; - flex-direction: column; - padding: 10px; - padding-top: 0px; - - .header { - h2 { - margin-bottom: 0; - padding-left: 10px; - color: $white; - } - - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - } - - .content { - - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: flex-start; - width: 100%; - border-radius: 4px; - gap: 10px; - - @media only screen and (max-width: 900px) { - flex-direction: column; - } - - .topRow { - display: flex; - flex-direction: row; - @media only screen and (max-width: 550px) { - flex-direction: column; - } - } - - .transferContainer, - .delegateContainer { - display: flex; - flex-direction: column; - justify-content: space-around; - align-items: flex-start; - width: 250px; - @media only screen and (max-width: 900px) { - width: 100%; - min-width: unset; - min-height: unset; - } - .info { - .title { - font-size: 1.4em; - color: $white; - } - .subTitle { - font-size: 1.0em; - color: blue; - margin: 10px; - } - .helpText { - font-size: 0.8em; - margin: 10px; - margin-bottom: 10px; - //opacity: 0.8; - } - .helpTextLight { - font-size: 0.7em; - margin: 10px; - opacity: 0.6; - text-align: left; - line-height: 1.0em; - margin-top: 20px; - padding: 20px; - } - } - } - } -} diff --git a/packages/boba/gateway/src/containers/ecosystem/Ecosystem.js b/packages/boba/gateway/src/containers/ecosystem/Ecosystem.js index 509ab9ec85..706821ae90 100644 --- a/packages/boba/gateway/src/containers/ecosystem/Ecosystem.js +++ b/packages/boba/gateway/src/containers/ecosystem/Ecosystem.js @@ -2,26 +2,30 @@ import React, { useEffect } from 'react' import * as S from './Ecosystem.styles' import Button from 'components/button/Button' import { Outlet, useNavigate, useParams } from 'react-router-dom' -import { ECOSYSTEM_CATEGORY } from 'util/constant' +import { ECOSYSTEM_CATEGORY, BOBA_PROJECTS_CATEGORY, ROUTES_PATH } from 'util/constant' -function ECOSYSTEM() { +function ECOSYSTEM({ ecosystemType }) { const navigate = useNavigate(); const params = useParams(); useEffect(() => { if (!params.category) { - navigate(`/ecosystem/defi`) + if (ecosystemType !== 'BOBA') { + navigate(`${ROUTES_PATH.ECOSYSTEM}/defi`) + } else { + navigate(`${ROUTES_PATH.BOBA_CHAINS}/mainnet`) + } } - }, [ params, navigate ]); + }, [ params, navigate, ecosystemType ]); return ( - {ECOSYSTEM_CATEGORY.map((cat, i) => { + {(ecosystemType !== 'BOBA' ? ECOSYSTEM_CATEGORY : BOBA_PROJECTS_CATEGORY).map((cat, i) => { return + + + ) +} + +export default React.memo(CastVoteModal) diff --git a/packages/boba/gateway/src/containers/modals/dao/NewProposalModal.js b/packages/boba/gateway/src/containers/modals/dao/NewProposalModal.js index b30c586050..4d0c152b42 100644 --- a/packages/boba/gateway/src/containers/modals/dao/NewProposalModal.js +++ b/packages/boba/gateway/src/containers/modals/dao/NewProposalModal.js @@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { Box, Typography } from '@mui/material' import { useDispatch, useSelector } from 'react-redux' @@ -23,251 +23,306 @@ import { closeModal, openAlert } from 'actions/uiAction' import Modal from 'components/modal/Modal' import Button from 'components/button/Button' import Input from 'components/input/Input' -// import Select from 'react-select' + import Select from 'components/select/Select' import { createDaoProposal } from 'actions/daoAction' import { selectProposalThreshold } from 'selectors/daoSelector' import BobaGlassIcon from 'components/icons/BobaGlassIcon' +import { selectLockRecords } from 'selectors/veBobaSelector' +import BobaNFTGlass from 'images/boba2/BobaNFTGlass.svg' function NewProposalModal({ open }) { - const dispatch = useDispatch() + const dispatch = useDispatch() - const [action, setAction] = useState('') - const [votingThreshold, setVotingThreshold] = useState('') + const [ action, setAction ] = useState('') + const [ selectedAction, setSelectedAction ] = useState('') + const [ tokens, setTokens ] = useState([]) + const [ votingThreshold, setVotingThreshold ] = useState('') - const [LPfeeMin, setLPfeeMin] = useState('') - const [LPfeeMax, setLPfeeMax] = useState('') - const [LPfeeOwn, setLPfeeOwn] = useState('') + const [ errorText, setErrorText ] = useState('') - const [proposeText, setProposeText] = useState('') - const [proposalUri, setProposalUri] = useState('') + const [ LPfeeMin, setLPfeeMin ] = useState('') + const [ LPfeeMax, setLPfeeMax ] = useState('') + const [ LPfeeOwn, setLPfeeOwn ] = useState('') - const loading = false //ToDo useSelector(selectLoading([ 'PROPOSAL_DAO/CREATE' ])) + const [ proposeText, setProposeText ] = useState('') + const [ proposalUri, setProposalUri ] = useState('') + const [ nftOptions, setNftOptions ] = useState([]); + const loading = false //ToDo useSelector(selectLoading([ 'PROPOSAL_DAO/CREATE' ])) - const proposalThreshold = useSelector(selectProposalThreshold) + const proposalThreshold = useSelector(selectProposalThreshold) + const records = useSelector(selectLockRecords); - const onActionChange = (e) => { - setVotingThreshold('') - setLPfeeMin('') - setLPfeeMax('') - setLPfeeOwn('') - setProposeText('') - setProposalUri('') - setAction(e.target.value) + useEffect(() => { + if (records && records.length > 0) { + const options = records.map((token) => ({ + value: token.tokenId, + balance: token.balance, + label: `#${token.tokenId}`, + title: `VeBoba - ${token.balance}`, + subTitle: `Lock Amount - ${token.lockedAmount}`, + icon: BobaNFTGlass + })) + setNftOptions(options); } - function handleClose() { - setVotingThreshold('') - setLPfeeMin('') - setLPfeeMax('') - setLPfeeOwn('') - setAction('') - setProposeText('') - setProposalUri('') - dispatch(closeModal('newProposalModal')) + return () => { + setNftOptions([]); + }; + }, [ records ]); + + useEffect(() => { + let tokensSum = tokens.reduce((c, i) => c + Number(i.balance), 0); + if (tokensSum < proposalThreshold) { + setErrorText(`Insufficient govBOBA to create a new proposal. You need at least ${proposalThreshold} govBOBA to create a proposal.`) + } else { + setErrorText('') } + },[tokens, proposalThreshold]) + + + const onActionChange = (e) => { + setVotingThreshold('') + setLPfeeMin('') + setLPfeeMax('') + setLPfeeOwn('') + setProposeText('') + setProposalUri('') + setSelectedAction(e) + setAction(e.value) + } + + function handleClose() { + setVotingThreshold('') + setLPfeeMin('') + setLPfeeMax('') + setLPfeeOwn('') + setAction('') + setProposeText('') + setProposalUri('') + dispatch(closeModal('newProposalModal')) + } + + const options = [ + { value: 'change-threshold', label: 'Change Voting Threshold' }, + { value: 'text-proposal', label: 'Freeform Text Proposal' }, + { value: 'change-lp1-fee', label: 'Change L1 LP fees' }, + { value: 'change-lp2-fee', label: 'Change L2 LP fees' } + ] - const options = [ - { value: 'change-threshold', label: 'Change Voting Threshold' ,title: 'Change Voting Threshold' }, - { value: 'text-proposal', label: 'Freeform Text Proposal' ,title: 'Freeform Text Proposal' }, - { value: 'change-lp1-fee', label: 'Change L1 LP fees' ,title: 'Change L1 LP fees' }, - { value: 'change-lp2-fee', label: 'Change L2 LP fees',title: 'Change L2 LP fees' } - ] - - const customStyles = { - option: (provided, state) => ({ - ...provided, - color: state.isSelected ? '#282828' : '#909090', - }), + const customStyles = { + option: (provided, state) => ({ + ...provided, + color: state.isSelected ? '#282828' : '#909090', + }), + } + + const submit = async () => { + + let res = null + let tokenIds = tokens.map((t) => t.value); + + if (action === 'change-threshold') { + res = await dispatch(createDaoProposal({ + action, + tokenIds, + value: [ votingThreshold ], + text: '' //extra text if any + })) + } else if (action === 'text-proposal') { + res = await dispatch(createDaoProposal({ + action, + tokenIds, + text: `${proposeText}@@${proposalUri}` + })) + } else if (action === 'change-lp1-fee' || action === 'change-lp2-fee') { + res = await dispatch(createDaoProposal({ + action, + tokenIds, + value: [ Math.round(Number(LPfeeMin) * 10), Math.round(Number(LPfeeMax) * 10), Math.round(Number(LPfeeOwn) * 10) ], + text: '' //extra text if any + })) } - const submit = async () => { - - let res = null - - if (action === 'change-threshold') { - res = await dispatch(createDaoProposal({ - action, - value: [votingThreshold], - text: '' //extra text if any - })) - } else if (action === 'text-proposal') { - res = await dispatch(createDaoProposal({ - action, - text: `${proposeText}@@${proposalUri}` - })) - } else if (action === 'change-lp1-fee' || action === 'change-lp2-fee') { - res = await dispatch(createDaoProposal({ - action, - value: [Math.round(Number(LPfeeMin)*10), Math.round(Number(LPfeeMax)*10), Math.round(Number(LPfeeOwn)*10)], - text: '' //extra text if any - })) - } - - if (res) { - dispatch(openAlert(`Proposal has been submitted. It will be listed soon`)) - } - handleClose() + if (res) { + dispatch(openAlert(`Proposal has been submitted. It will be listed soon`)) } + handleClose() + } - const disabled = () => { - if (action === 'change-threshold') { - return !votingThreshold - } else if (action === 'text-proposal') { - return !proposeText - } else if (action === 'change-lp1-fee' || action === 'change-lp2-fee') { - if (Number(LPfeeMin) < 0.0 || Number(LPfeeMin) > 5.0) { - return true //aka disabled - } - if (Number(LPfeeMax) < 0.0 || Number(LPfeeMax) > 5.0) { - return true //aka disabled - } - if (Number(LPfeeMax) <= Number(LPfeeMin)) { - return true //aka disabled - } - if (Number(LPfeeOwn) < 0.0 || Number(LPfeeOwn) > 5.0) { - return true - } - if (LPfeeMin === '') { - return true - } - if (LPfeeMax === '') { - return true - } - if (LPfeeOwn === '') { - return true - } - return false - } + const disabled = () => { + if (!proposalThreshold) { + return true + } + if (action === 'change-threshold') { + return !votingThreshold + } else if (action === 'text-proposal') { + return !proposeText + } else if (action === 'change-lp1-fee' || action === 'change-lp2-fee') { + if (Number(LPfeeMin) < 0.0 || Number(LPfeeMin) > 5.0) { + return true //aka disabled + } + if (Number(LPfeeMax) < 0.0 || Number(LPfeeMax) > 5.0) { + return true //aka disabled + } + if (Number(LPfeeMax) <= Number(LPfeeMin)) { + return true //aka disabled + } + if (Number(LPfeeOwn) < 0.0 || Number(LPfeeOwn) > 5.0) { + return true + } + if (LPfeeMin === '') { + return true + } + if (LPfeeMax === '') { + return true + } + if (LPfeeOwn === '') { + return true + } + return false } + } - return ( - - - - - - New Proposal - - - - - {action === '' && - - Currently, the DAO can change the voting threshold, propose free-form text proposals, and - change to the bridge fee limits for the L1 and L2 bridge pools. - - } - - {action === 'change-threshold' && - <> - - The minimum number of votes required for an account to create a proposal. The current value is {proposalThreshold}. - - setVotingThreshold(i.target.value)} - fullWidth - sx={{marginBottom: '20px'}} - /> - - } - {(action === 'change-lp1-fee' || action === 'change-lp2-fee') && - <> - - Possible settings range from 0.0% to 5.0%. - All three values must be specified and the maximum fee must be larger than the minimum fee. - - setLPfeeMin(i.target.value)} - fullWidth - /> - setLPfeeMax(i.target.value)} - fullWidth - /> - setLPfeeOwn(i.target.value)} - fullWidth - /> - - } - {action === 'text-proposal' && - <> - - Your proposal title is limited to 100 characters. Use the link field below to provide more information. - - setProposeText(i.target.value.slice(0, 100))} - /> - - You should provide additional information (technical specifications, diagrams, forum threads, and other material) on a seperate - website. The link length is limited to 150 characters. You may need to use a link shortener. - - setProposalUri(i.target.value.slice(0, 150))} - /> - - } - + return ( + + + + + + New Proposal + - - + + + {action === '' && + + Currently, the DAO can change the voting threshold, propose free-form text proposals, and + change to the bridge fee limits for the L1 and L2 bridge pools. + + } + + {action === 'change-threshold' && + <> + + The minimum number of votes required for an account to create a proposal. The current value is {proposalThreshold}. + + setVotingThreshold(i.target.value)} + fullWidth + sx={{ marginBottom: '20px' }} + /> + + } + {(action === 'change-lp1-fee' || action === 'change-lp2-fee') && + <> + + Possible settings range from 0.0% to 5.0%. + All three values must be specified and the maximum fee must be larger than the minimum fee. + + setLPfeeMin(i.target.value)} + fullWidth + /> + setLPfeeMax(i.target.value)} + fullWidth + /> + setLPfeeOwn(i.target.value)} + fullWidth + /> + + } + {action === 'text-proposal' && + <> + + Your proposal title is limited to 100 characters. Use the link field below to provide more information. + + setProposeText(i.target.value.slice(0, 100))} + /> + + You should provide additional information (technical specifications, diagrams, forum threads, and other material) on a seperate + website. The link length is limited to 150 characters. You may need to use a link shortener. + + setProposalUri(i.target.value.slice(0, 150))} + /> + + } + {errorText ? {errorText} : null} - - ) + + + + + + ) } export default React.memo(NewProposalModal) diff --git a/packages/boba/gateway/src/containers/modals/deposit/DepositModal.js b/packages/boba/gateway/src/containers/modals/deposit/DepositModal.js index 29d365e16b..4faf6d4033 100644 --- a/packages/boba/gateway/src/containers/modals/deposit/DepositModal.js +++ b/packages/boba/gateway/src/containers/modals/deposit/DepositModal.js @@ -21,7 +21,9 @@ import { closeModal } from 'actions/uiAction' import InputStep from './steps/InputStep' import InputStepFast from './steps/InputStepFast' +import InputStepMultiChain from './steps/InputStepMultiChain' import { fetchTransactions } from 'actions/networkAction' +import { BRIDGE_TYPE } from 'util/constant' function DepositModal({ open, token, fast }) { @@ -34,11 +36,22 @@ function DepositModal({ open, token, fast }) { return ( - {!!fast ? ( + + { + BRIDGE_TYPE.FAST_BRIDGE === fast ? : null + } + { + BRIDGE_TYPE.CLASSIC_BRIDGE === fast ? : null + } + { + BRIDGE_TYPE.MULTI_CHAIN_BRIDGE === fast ? : null + } + + {/* {!!fast ? ( ) : ( - )} + )} */} ) } diff --git a/packages/boba/gateway/src/containers/modals/deposit/steps/InputStep.js b/packages/boba/gateway/src/containers/modals/deposit/steps/InputStep.js index 7689cf76b8..e745bae1b9 100644 --- a/packages/boba/gateway/src/containers/modals/deposit/steps/InputStep.js +++ b/packages/boba/gateway/src/containers/modals/deposit/steps/InputStep.js @@ -66,7 +66,7 @@ function InputStep({ handleClose, token, isBridge, openTokenPicker }) { ) } if (res) { - dispatch(setActiveHistoryTab('Bridge to L2')) + dispatch(setActiveHistoryTab('Ethereum to Boba Ethereum L2')) handleClose() } diff --git a/packages/boba/gateway/src/containers/modals/deposit/steps/InputStepBatch.js b/packages/boba/gateway/src/containers/modals/deposit/steps/InputStepBatch.js index 49a1c54fa9..2416d49b25 100644 --- a/packages/boba/gateway/src/containers/modals/deposit/steps/InputStepBatch.js +++ b/packages/boba/gateway/src/containers/modals/deposit/steps/InputStepBatch.js @@ -101,7 +101,7 @@ function InputStepBatch({ isBridge, handleClose }) { ) if (res) { - dispatch(setActiveHistoryTab('Bridge to L2')) + dispatch(setActiveHistoryTab('Boba Ethereum L2 to Ethereum')) dispatch( openAlert( `Your funds were bridged to the L1LP in batch.` diff --git a/packages/boba/gateway/src/containers/modals/deposit/steps/InputStepFast.js b/packages/boba/gateway/src/containers/modals/deposit/steps/InputStepFast.js index 5079d6fdb7..9565490e57 100644 --- a/packages/boba/gateway/src/containers/modals/deposit/steps/InputStepFast.js +++ b/packages/boba/gateway/src/containers/modals/deposit/steps/InputStepFast.js @@ -159,7 +159,7 @@ function InputStepFast({ handleClose, token, isBridge, openTokenPicker }) { res = await dispatch(depositL1LP(token.address, value_Wei_String)) if (res) { - dispatch(setActiveHistoryTab('Bridge to L2')) + dispatch(setActiveHistoryTab('Ethereum to Boba Ethereum L2')) dispatch( openAlert( `ETH was bridged. You will receive approximately @@ -195,7 +195,7 @@ function InputStepFast({ handleClose, token, isBridge, openTokenPicker }) { ) if (res) { - dispatch(setActiveHistoryTab('Bridge to L2')) + dispatch(setActiveHistoryTab('Ethereum to Boba Ethereum L2')) dispatch( openAlert( `${token.symbol} was bridged to the L1LP. You will receive approximately diff --git a/packages/boba/gateway/src/containers/modals/deposit/steps/InputStepMultiChain.js b/packages/boba/gateway/src/containers/modals/deposit/steps/InputStepMultiChain.js new file mode 100644 index 0000000000..5b9bb676ca --- /dev/null +++ b/packages/boba/gateway/src/containers/modals/deposit/steps/InputStepMultiChain.js @@ -0,0 +1,255 @@ + +import React, { useState, useEffect } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { depositErc20ToL1 } from 'actions/networkAction' +import { openAlert, setActiveHistoryTab } from 'actions/uiAction' + +import Button from 'components/button/Button' +import Input from 'components/input/Input' + +import { selectLoading } from 'selectors/loadingSelector' +import { selectSignatureStatus_depositTRAD } from 'selectors/signatureSelector' +import { + amountToUsd, logAmount, + // toWei_String +} from 'util/amountConvert' +import { getCoinImage } from 'util/coinImage' + +import { selectLookupPrice } from 'selectors/lookupSelector' +import { Box, Typography, useMediaQuery } from '@mui/material' +import { useTheme } from '@emotion/react' +import { WrapperActionsModal } from 'components/modal/Modal.styles' + +import BN from 'bignumber.js' +import parse from 'html-react-parser' +import Select from 'components/select/Select' +import { selectAltL1DepositCost, selectL1FeeBalance } from 'selectors/balanceSelector' +import { fetchAltL1DepositFee, fetchL1FeeBalance } from 'actions/balanceAction' + +import networkService from 'services/networkService' + +/** + * @NOTE + * Cross Chain Bridging to alt L1 is only supported for BOBA as of now! + * + */ + +function InputStepMultiChain({ handleClose, token, isBridge, openTokenPicker }) { + + const getImageComponent = (symbol) => { + return {symbol} + } + + const options = [ + { value: 'BNB', label: 'BNB', title: 'BNB', image: getImageComponent("BNB") }, + { value: 'Avalanche', label: 'Avalanche', title: 'Avalanche', image: getImageComponent('AVAX') }, + { value: 'Fantom', label: 'Fantom', title: 'Fantom', image: getImageComponent('FTM') }, + { value: 'Moonbeam', label: 'Moonbeam', title: 'Moonbeam', image: getImageComponent('GLMR') }, + ].filter(i => networkService.supportedAltL1Chains.includes(i.value)) + + const dispatch = useDispatch() + + const [ value, setValue ] = useState('') + const [ altL1Bridge, setAltL1Bridge ] = useState('') + + const [ validValue, setValidValue ] = useState(false) + const depositLoading = useSelector(selectLoading([ 'DEPOSIT_ALTL1/CREATE' ])) + + const signatureStatus = useSelector(selectSignatureStatus_depositTRAD) + const lookupPrice = useSelector(selectLookupPrice) + const depositFees = useSelector(selectAltL1DepositCost) + const feeBalance = useSelector(selectL1FeeBalance) //amount of ETH on L1 to pay gas + + const maxValue = logAmount(token.balance, token.decimals) + + function setAmount(value) { + + const tooSmall = new BN(value).lte(new BN(0.0)) + const tooBig = new BN(value).gt(new BN(maxValue)) + + if (tooSmall || tooBig) { + setValidValue(false) + } else { + setValidValue(true) + } + + setValue(value) + } + + async function doDeposit() { + + const res = await dispatch(depositErc20ToL1({ + value: value, + type: altL1Bridge + })) + + if (res) { + dispatch(openAlert(`Successfully bridge ${token.symbol} to alt L1 ${altL1Bridge}!`)) + dispatch(setActiveHistoryTab('Bridge between L1s')) + handleClose() + } else { + console.log(`🤦 opps something wrong!`) + handleClose() + } + } + + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down('md')) + + useEffect(() => { + dispatch(fetchL1FeeBalance()) //ETH balance for paying gas + dispatch(fetchAltL1DepositFee()) + },[dispatch]) + + useEffect(() => { + if (signatureStatus && depositLoading) { + //we are all set - can close the window + //transaction has been sent and signed + handleClose() + } + }, [ signatureStatus, depositLoading, handleClose ]) + + let buttonLabel_1 = 'Cancel' + if (depositLoading) buttonLabel_1 = 'Close' + + let convertToUSD = false + + if (Object.keys(lookupPrice) && + !!value && + validValue && + !!amountToUsd(value, lookupPrice, token) + ) { + convertToUSD = true + } + + if (Number(logAmount(token.balance, token.decimals)) === 0) { + //no token in this account + return ( + + + Sorry, nothing to deposit - no {token.symbol} in this wallet + + + ) + } + + const onBridgeChange = (e) => { + setAltL1Bridge(e.target.value) + } + + const customStyles = { + option: (provided, state) => ({ + ...provided, + color: state.isSelected ? '#282828' : '#909090', + }), + } + + let ETHstring = '' + let warning = false + + if (depositFees[altL1Bridge]) { + if(Number(depositFees[altL1Bridge].fee) > Number(feeBalance)) { + warning = true + ETHstring = `WARNING: your L1 ${networkService.L1NativeTokenSymbol} balance of ${Number(feeBalance).toFixed(4)} is not sufficient to cover this transaction.` + } + } + + return ( + <> + + + Bridge {token && token.symbol ? token.symbol : ''} to Alt L1s + + + + { + setAmount(i.target.value) + // setValue_Wei_String(toWei_String(i.target.value, token.decimals)) + }} + onUseMax={(i) => {//they want to use the maximum + setAmount(maxValue) //so the input value updates for the user - just for display purposes + // setValue_Wei_String(token.balance.toString()) //this is the one that matters + }} + allowUseAll={true} + unit={token.symbol} + maxValue={maxValue} + variant="standard" + newStyle + isBridge={isBridge} + openTokenPicker={openTokenPicker} + /> + + + {!!altL1Bridge && depositFees? <> + + Estimated fee for bridging {value} {token.symbol} is {depositFees[altL1Bridge].fee} ETH + + : null} + + {!!convertToUSD && ( + + {`Amount in USD ${amountToUsd(value, lookupPrice, token).toFixed(2)}`} + + )} + + {warning && ( + + {parse(ETHstring)} + + )} + + + + + + + + ) +} + +export default React.memo(InputStepMultiChain) diff --git a/packages/boba/gateway/src/containers/modals/veBoba/ManageLockModal.styles.js b/packages/boba/gateway/src/containers/modals/veBoba/ManageLockModal.styles.js index 2e2c7285af..8c5797244b 100644 --- a/packages/boba/gateway/src/containers/modals/veBoba/ManageLockModal.styles.js +++ b/packages/boba/gateway/src/containers/modals/veBoba/ManageLockModal.styles.js @@ -5,17 +5,6 @@ export const Container = styled(Box)(({ theme }) => ({ })) -export const ThumbnailContainer = styled(Box)(({ theme }) => ({ - background: theme.palette.background.secondary, - borderRadius: theme.palette.primary.borderRadius, - border: '1px solid rgba(255, 255, 255, 0.15)', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: '4rem', - width: '4rem', -})) - export const LockFormContainer = styled(Grid)(({ theme }) => ({ borderRadius: theme.palette.primary.borderRadius, background: theme.palette.background.secondary, diff --git a/packages/boba/gateway/src/containers/modals/veBoba/WithdrawLock.js b/packages/boba/gateway/src/containers/modals/veBoba/WithdrawLock.js index 54eb8b25ce..4858e6ebbf 100644 --- a/packages/boba/gateway/src/containers/modals/veBoba/WithdrawLock.js +++ b/packages/boba/gateway/src/containers/modals/veBoba/WithdrawLock.js @@ -9,7 +9,8 @@ import Button from 'components/button/Button'; import BobaNFTGlass from 'images/boba2/BobaNFTGlass.svg'; -import * as S from './ManageLockModal.styles'; +import * as G from 'containers/Global.styles'; + import moment from 'moment'; function WithdrawLock({ @@ -36,9 +37,9 @@ function WithdrawLock({ return - + glass - + NFT ID: diff --git a/packages/boba/gateway/src/containers/veboba/Lock.js b/packages/boba/gateway/src/containers/veboba/Lock.js index e132b22c76..318b290234 100644 --- a/packages/boba/gateway/src/containers/veboba/Lock.js +++ b/packages/boba/gateway/src/containers/veboba/Lock.js @@ -9,7 +9,7 @@ import * as S from './Lock.styles' import CreateLock from './createLock/CreateLock' import LockRecords from './Records/Records' import { useDispatch, useSelector } from 'react-redux' -import { selectAccountEnabled } from 'selectors/setupSelector' +import { selectAccountEnabled, selectLayer } from 'selectors/setupSelector' const data = [ { name: '0', uv: 0 }, @@ -22,12 +22,13 @@ const data = [ function Lock() { const dispatch = useDispatch(); const accountEnabled = useSelector(selectAccountEnabled()) + const layer = useSelector(selectLayer()) useEffect(() => { - if (!!accountEnabled) { + if (!!accountEnabled && layer === 'L2') { dispatch(fetchLockRecords()); } - }, [ accountEnabled, dispatch ]); + }, [ accountEnabled,layer, dispatch ]); return diff --git a/packages/boba/gateway/src/containers/veboba/Records/RecordItem.js b/packages/boba/gateway/src/containers/veboba/Records/RecordItem.js index 44b64c81b8..a0d542ab86 100644 --- a/packages/boba/gateway/src/containers/veboba/Records/RecordItem.js +++ b/packages/boba/gateway/src/containers/veboba/Records/RecordItem.js @@ -1,9 +1,9 @@ import { Box, Grid, Typography } from '@mui/material' import Button from 'components/button/Button' +import * as G from 'containers/Global.styles' import BobaNFTGlass from 'images/boba2/BobaNFTGlass.svg' import moment from 'moment' import React from 'react' -import * as S from './RecordItem.styles' function RecordItem({ onManage, @@ -32,9 +32,9 @@ function RecordItem({ return - + glass - + #{tokenId} @@ -48,13 +48,13 @@ function RecordItem({ - {lockedAmount} + {lockedAmount.toFixed(2)} Boba - {balance} + {balance.toFixed(2)} veBoba diff --git a/packages/boba/gateway/src/containers/veboba/Records/RecordItem.styles.js b/packages/boba/gateway/src/containers/veboba/Records/RecordItem.styles.js deleted file mode 100644 index 6752b535c4..0000000000 --- a/packages/boba/gateway/src/containers/veboba/Records/RecordItem.styles.js +++ /dev/null @@ -1,12 +0,0 @@ -import { Box } from "@mui/material"; -import { styled } from '@mui/material/styles'; - -export const ThumbnailContainer = styled(Box)(({ theme }) => ({ - background: theme.palette.background.secondary, - borderRadius: theme.palette.primary.borderRadius, - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: '4rem', - width: '4rem', -})) diff --git a/packages/boba/gateway/src/containers/veboba/createLock/CreateLock.js b/packages/boba/gateway/src/containers/veboba/createLock/CreateLock.js index 36c7472e14..6bafef52e2 100644 --- a/packages/boba/gateway/src/containers/veboba/createLock/CreateLock.js +++ b/packages/boba/gateway/src/containers/veboba/createLock/CreateLock.js @@ -168,10 +168,10 @@ function CreateLock({ Your voting power will be - {conversioRation()* value } ve BOBA + {(conversioRation() * value).toFixed(2) } ve BOBA { - !accountEnabled ? + !accountEnabled || layer !== 'L2'?