diff --git a/.env.defaults b/.env.defaults index d2914b14b4..c9d03e485d 100644 --- a/.env.defaults +++ b/.env.defaults @@ -30,4 +30,9 @@ SUPPORT_ARBITRUM_NOVA=false SUPPORT_SWAP_QUOTE_REFRESH=false SUPPORT_ACHIEVEMENTS_BANNER=false SUPPORT_NFT_SEND=false +SUPPORT_THE_ISLAND=true +SUPPORT_THE_ISLAND_ON_TENDERLY=false USE_MAINNET_FORK=false +ARBITRUM_FORK_RPC=https://rpc.tenderly.co/fork/2fc2cf12-5c58-439f-9b5e-967bfd02191a +TESTNET_TAHO_DEPLOYER_ADDRESS=0x55B180c3470dA8E31761d45468e4E61DbE13Eb9B +TESTNET_TAHO_ADDRESS=0x78f04eC76df38Fcb37971Efa8EcbcB33f52dae0F diff --git a/background/features.ts b/background/features.ts index ecf56afa96..e4a9b5c56e 100644 --- a/background/features.ts +++ b/background/features.ts @@ -23,6 +23,9 @@ export const RuntimeFlag = { SUPPORT_SWAP_QUOTE_REFRESH: process.env.SUPPORT_SWAP_QUOTE_REFRESH === "true", SUPPORT_CUSTOM_NETWORKS: process.env.SUPPORT_CUSTOM_NETWORKS === "true", SUPPORT_CUSTOM_RPCS: process.env.SUPPORT_CUSTOM_RPCS === "true", + SUPPORT_THE_ISLAND: process.env.SUPPORT_THE_ISLAND === "true", + SUPPORT_THE_ISLAND_ON_TENDERLY: + process.env.SUPPORT_THE_ISLAND_ON_TENDERLY === "true", } as const type BuildTimeFlagType = keyof typeof BuildTimeFlag diff --git a/background/main.ts b/background/main.ts index bdf071c310..aedec6078d 100644 --- a/background/main.ts +++ b/background/main.ts @@ -29,7 +29,7 @@ import { ProviderBridgeService, TelemetryService, ServiceCreatorFunction, - DoggoService, + IslandService, LedgerService, SigningService, NFTsService, @@ -41,7 +41,7 @@ import { import { HexString, NormalizedEVMAddress } from "./types" import { SignedTransaction } from "./networks" import { AccountBalance, AddressOnNetwork, NameOnNetwork } from "./accounts" -import { Eligible } from "./services/doggo/types" +import { Eligible, ReferrerStats } from "./services/island/types" import rootReducer from "./redux-slices" import { @@ -58,6 +58,7 @@ import { removeAssetData, } from "./redux-slices/assets" import { + addIslandAsset, setEligibility, setEligibilityLoading, setReferrer, @@ -135,7 +136,6 @@ import { SignatureResponse, TXSignatureResponse, } from "./services/signing" -import { ReferrerStats } from "./services/doggo/db" import { migrateReduxState, REDUX_STATE_VERSION, @@ -327,7 +327,7 @@ export default class Main extends BaseService { internalEthereumProviderService, preferenceService, ) - const doggoService = DoggoService.create(chainService, indexingService) + const islandService = IslandService.create(chainService, indexingService) const telemetryService = TelemetryService.create() @@ -399,7 +399,7 @@ export default class Main extends BaseService { await nameService, await internalEthereumProviderService, await providerBridgeService, - await doggoService, + await islandService, await telemetryService, await ledgerService, await signingService, @@ -458,7 +458,7 @@ export default class Main extends BaseService { * A promise to the claim service, which saves the eligibility data * for efficient storage and retrieval. */ - private doggoService: DoggoService, + private islandService: IslandService, /** * A promise to the telemetry service, which keeps track of extension * storage usage and (eventually) other statistics. @@ -602,7 +602,7 @@ export default class Main extends BaseService { this.nameService.startService(), this.internalEthereumProviderService.startService(), this.providerBridgeService.startService(), - this.doggoService.startService(), + this.islandService.startService(), this.telemetryService.startService(), this.ledgerService.startService(), this.signingService.startService(), @@ -625,7 +625,7 @@ export default class Main extends BaseService { this.nameService.stopService(), this.internalEthereumProviderService.stopService(), this.providerBridgeService.stopService(), - this.doggoService.stopService(), + this.islandService.stopService(), this.telemetryService.stopService(), this.ledgerService.stopService(), this.signingService.stopService(), @@ -647,7 +647,7 @@ export default class Main extends BaseService { this.connectProviderBridgeService() this.connectPreferenceService() this.connectEnrichmentService() - this.connectDoggoService() + this.connectIslandService() this.connectTelemetryService() this.connectLedgerService() this.connectSigningService() @@ -840,6 +840,9 @@ export default class Main extends BaseService { // Force a refresh of the account balance to populate the store. this.chainService.getLatestBaseAccountBalance(addressNetwork) }) + + // Set up Island Monitoring + await this.islandService.startMonitoringIfNeeded() }) // Wire up chain service to account slice. @@ -1584,14 +1587,14 @@ export default class Main extends BaseService { this.store.dispatch(clearSwapQuote()) this.store.dispatch(setEligibilityLoading()) - this.doggoService.getEligibility(addressNetwork.address) + this.islandService.getEligibility(addressNetwork.address) this.store.dispatch(setVaultsAsStale()) await this.chainService.markAccountActivity(addressNetwork) const referrerStats = - await this.doggoService.getReferrerStats(addressNetwork) + await this.islandService.getReferrerStats(addressNetwork) this.store.dispatch(setReferrerStats(referrerStats)) this.providerBridgeService.notifyContentScriptsAboutAddressChange( @@ -1629,15 +1632,15 @@ export default class Main extends BaseService { }) } - async connectDoggoService(): Promise { - this.doggoService.emitter.on( + async connectIslandService(): Promise { + this.islandService.emitter.on( "newEligibility", async (eligibility: Eligible) => { await this.store.dispatch(setEligibility(eligibility)) }, ) - this.doggoService.emitter.on( + this.islandService.emitter.on( "newReferral", async ( referral: { @@ -1660,6 +1663,10 @@ export default class Main extends BaseService { } }, ) + + this.islandService.emitter.on("monitoringTestnetAsset", (asset) => { + this.store.dispatch(addIslandAsset(asset)) + }) } connectTelemetryService(): void { diff --git a/background/redux-slices/accounts.ts b/background/redux-slices/accounts.ts index 0754c30bc6..80bb2533d1 100644 --- a/background/redux-slices/accounts.ts +++ b/background/redux-slices/accounts.ts @@ -423,7 +423,7 @@ const accountSlice = createSlice({ { payload: asset }: { payload: SmartContractFungibleAsset }, ) => { const allAccounts = immerState.accountsData.evm[asset.homeNetwork.chainID] - Object.keys(allAccounts).forEach((address) => { + Object.keys(allAccounts ?? {}).forEach((address) => { const account = allAccounts[address] if (account !== "loading") { Object.values(account.balances).forEach(({ assetAmount }) => { diff --git a/background/redux-slices/claim.ts b/background/redux-slices/claim.ts index f40a2a8917..cfe25b1211 100644 --- a/background/redux-slices/claim.ts +++ b/background/redux-slices/claim.ts @@ -1,7 +1,7 @@ import { createSlice, createSelector } from "@reduxjs/toolkit" import { BigNumber, Signature, utils } from "ethers" import { TransactionResponse } from "@ethersproject/abstract-provider" -import { Eligible } from "../services/doggo/types" +import { Eligible } from "../services/island/types" import { createBackgroundAsyncThunk } from "./utils" import { normalizeEVMAddress, truncateAddress } from "../lib/utils" @@ -19,8 +19,17 @@ import DISTRIBUTOR_ABI from "./contract-abis/merkle-distributor" import { DOGGO, HOUR } from "../constants" import { FeatureFlags, isEnabled } from "../features" import { ERC2612_INTERFACE } from "../lib/erc20" -import { ReferrerStats } from "../services/doggo/db" +import { + ReferrerStats, + TESTNET_TAHO, + VOTE_WITH_FRIENDS_ADDRESS, +} from "../services/island" import { fromFixedPointNumber } from "../lib/fixed-point" +import { SmartContractFungibleAsset } from "../assets" +import { isSameAsset } from "./utils/asset-utils" +import { selectCurrentAccount } from "./selectors/uiSelectors" +import { AccountState } from "./accounts" +import { ISLAND_NETWORK } from "../services/island/contracts" export interface DAO { address: string @@ -44,6 +53,7 @@ export interface Referrer { interface ClaimingState { status: string + islandAssets: SmartContractFungibleAsset[] | undefined claimed: { [address: HexString]: boolean } @@ -64,8 +74,6 @@ interface ClaimingState { } const DOGGO_TOKEN_ADDRESS = DOGGO.contractAddress -export const VOTE_WITH_FRIENDS_ADDRESS = - "0x0036B3a9D385Ce2CC072cf4A26dE29aE3283DEd0" const getDistributorContract = async () => { const distributorContractAddress = VOTE_WITH_FRIENDS_ADDRESS // VoteWithFriends contract address @@ -78,6 +86,7 @@ const getDistributorContract = async () => { const initialState: ClaimingState = { status: "idle", + islandAssets: [], claimed: {}, selectedForBonus: null, selectedDelegate: null, @@ -150,6 +159,16 @@ const claimingSlice = createSlice({ nonce, expiry, }), + addIslandAsset: ( + immerState, + { payload: asset }: { payload: SmartContractFungibleAsset }, + ) => { + if (immerState.islandAssets === undefined) { + immerState.islandAssets = [asset] + } else { + immerState.islandAssets.push(asset) + } + }, resetClaimFlow: (immerState) => { immerState.signature = undefined immerState.selectedForBonus = null @@ -179,6 +198,7 @@ const claimingSlice = createSlice({ }) export const { + addIslandAsset, chooseSelectedForBonus, chooseDelegate, setEligibility, @@ -426,3 +446,47 @@ export const selectEligibilityLoading = createSelector( (state: { claim: ClaimingState }): ClaimingState => state.claim, (claimState: ClaimingState) => claimState.eligibilityLoading, ) + +export const selectIsTestTahoDeployed = createSelector( + (state: { claim: ClaimingState }): ClaimingState => state.claim, + (claimState: ClaimingState) => + claimState.islandAssets?.some((asset) => + isSameAsset(asset, TESTNET_TAHO), + ) ?? false, +) + +export const selectHasIslandAssets = createSelector( + [ + (state: { claim: ClaimingState }): ClaimingState => state.claim, + (state: { account: AccountState }): AccountState => state.account, + selectIsTestTahoDeployed, + selectCurrentAccount, + ], + (claimState, accountState, isTahoDeployed, { address }) => { + const { islandAssets } = claimState + + const currentAccountData = + accountState.accountsData.evm[ISLAND_NETWORK.chainID]?.[ + normalizeEVMAddress(address) + ] + + if ( + !isTahoDeployed || + islandAssets === undefined || + islandAssets.length === 0 || + !currentAccountData || + currentAccountData === "loading" + ) { + return false + } + + const balances = Object.values(currentAccountData?.balances ?? {}) + + const hasIslandAssets = islandAssets.some((islandAsset) => + balances.some(({ assetAmount: { asset } }) => + isSameAsset(asset, islandAsset), + ), + ) + return hasIslandAssets + }, +) diff --git a/background/redux-slices/selectors/accountsSelectors.ts b/background/redux-slices/selectors/accountsSelectors.ts index fb933b4dba..282e907e79 100644 --- a/background/redux-slices/selectors/accountsSelectors.ts +++ b/background/redux-slices/selectors/accountsSelectors.ts @@ -13,6 +13,7 @@ import { formatCurrencyAmount, heuristicDesiredDecimalsForUnitPrice, isNetworkBaseAsset, + isSameAsset, isTrustedAsset, } from "../utils/asset-utils" import { @@ -45,6 +46,7 @@ import { AccountSigner, SignerType } from "../../services/signing" import { SignerImportSource } from "../../services/internal-signer" import { assertUnreachable } from "../../lib/utils/type-guards" import { PricesState, selectAssetPricePoint } from "../prices" +import { TESTNET_TAHO } from "../../services/island" // TODO What actual precision do we want here? Probably more than 2 // TODO decimals? Maybe it's configurable? @@ -64,11 +66,12 @@ export const userValueDustThreshold = 2 const shouldForciblyDisplayAsset = ( assetAmount: CompleteAssetAmount, ) => { - const isDoggo = - !isEnabled(FeatureFlags.HIDE_TOKEN_FEATURES) && - assetAmount.asset.symbol === DOGGO.symbol + const isIslandRelated = + (!isEnabled(FeatureFlags.HIDE_TOKEN_FEATURES) && + assetAmount.asset.symbol === DOGGO.symbol) || + isSameAsset(assetAmount.asset, TESTNET_TAHO) - return isDoggo || isNetworkBaseAsset(assetAmount.asset) + return isIslandRelated || isNetworkBaseAsset(assetAmount.asset) } export function determineAssetDisplayAndVerify( diff --git a/background/services/chain/index.ts b/background/services/chain/index.ts index 34e201e5b6..d436699c0f 100644 --- a/background/services/chain/index.ts +++ b/background/services/chain/index.ts @@ -70,6 +70,7 @@ import { } from "./utils/optimismGasPriceOracle" import InternalSignerService, { SignerImportSource } from "../internal-signer" import type { ValidatedAddEthereumChainParameter } from "../provider-bridge/utils" +import { ISLAND_NETWORK } from "../island/contracts" // The number of blocks to query at a time for historic asset transfers. // Unfortunately there's no "right" answer here that works well across different @@ -875,10 +876,19 @@ export default class ChainService extends BaseService { async getNetworksToTrack(): Promise { const chainIDs = await this.db.getChainIDsToTrack() if (chainIDs.size === 0) { - // Default to tracking Ethereum so ENS resolution works during onboarding + // Ethereum - default to tracking Ethereum so ENS resolution works during onboarding + // Arbitrum Sepolia - default to tracking so we can support Island Dapp + if (isEnabled(FeatureFlags.SUPPORT_THE_ISLAND)) { + return [ETHEREUM, ISLAND_NETWORK] + } + return [ETHEREUM] } + if (isEnabled(FeatureFlags.SUPPORT_THE_ISLAND)) { + chainIDs.add(ISLAND_NETWORK.chainID) + } + const networks = await Promise.all( [...chainIDs].map(async (chainID) => { const network = NETWORK_BY_CHAIN_ID[chainID] @@ -888,6 +898,7 @@ export default class ChainService extends BaseService { return network }), ) + return networks.filter((network): network is EVMNetwork => !!network) } diff --git a/background/services/chain/serial-fallback-provider.ts b/background/services/chain/serial-fallback-provider.ts index 9abc72ef8a..1e16d33bba 100644 --- a/background/services/chain/serial-fallback-provider.ts +++ b/background/services/chain/serial-fallback-provider.ts @@ -14,6 +14,7 @@ import { ARBITRUM_ONE, OPTIMISM, FORK, + ARBITRUM_SEPOLIA, } from "../../constants" import logger from "../../lib/logger" import { AnyEVMTransaction } from "../../networks" @@ -1090,6 +1091,25 @@ export function makeSerialFallbackProvider( ]) } + if ( + chainID === ARBITRUM_SEPOLIA.chainID && + process.env.ARBITRUM_FORK_RPC !== undefined && + process.env.ARBITRUM_FORK_RPC.trim() !== "" && + process.env.SUPPORT_THE_ISLAND_ON_TENDERLY === "true" + ) { + // eslint-disable-next-line no-console + console.log( + "%c🦴 Using Tenderly fork as Arbitrum Sepolia provider", + "background: #071111; color: #fff; font-weight: 900;", + ) + return new SerialFallbackProvider(ARBITRUM_SEPOLIA.chainID, [ + { + type: "generic" as const, + creator: () => new JsonRpcBatchProvider(process.env.ARBITRUM_FORK_RPC), + }, + ]) + } + const alchemyProviderCreators: ProviderCreator[] = ALCHEMY_SUPPORTED_CHAIN_IDS.has(chainID) ? [ diff --git a/background/services/doggo/contracts.ts b/background/services/doggo/contracts.ts deleted file mode 100644 index b33b966fee..0000000000 --- a/background/services/doggo/contracts.ts +++ /dev/null @@ -1,282 +0,0 @@ -import { ethers } from "ethers" -import { VOTE_WITH_FRIENDS_ADDRESS } from "../../redux-slices/claim" - -export const CLAIM_WITH_FRIENDS_ABI = [ - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "uint256", - name: "index", - type: "uint256", - }, - { - indexed: false, - internalType: "address", - name: "account", - type: "address", - }, - { - indexed: false, - internalType: "uint256", - name: "amount", - type: "uint256", - }, - ], - name: "Claimed", - type: "event", - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "uint256", - name: "index", - type: "uint256", - }, - { - indexed: true, - internalType: "address", - name: "claimant", - type: "address", - }, - { - indexed: false, - internalType: "uint256", - name: "amountClaimed", - type: "uint256", - }, - { - indexed: false, - internalType: "uint256", - name: "claimedBonus", - type: "uint256", - }, - { - indexed: true, - internalType: "address", - name: "communityRef", - type: "address", - }, - { - indexed: false, - internalType: "uint256", - name: "communityBonus", - type: "uint256", - }, - ], - name: "ClaimedWithCommunityCode", - type: "event", - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "address", - name: "previousOwner", - type: "address", - }, - { - indexed: true, - internalType: "address", - name: "newOwner", - type: "address", - }, - ], - name: "OwnershipTransferred", - type: "event", - }, - { - inputs: [ - { - internalType: "uint256", - name: "index", - type: "uint256", - }, - { - internalType: "address", - name: "account", - type: "address", - }, - { - internalType: "uint256", - name: "amount", - type: "uint256", - }, - { - internalType: "bytes32[]", - name: "merkleProof", - type: "bytes32[]", - }, - ], - name: "claim", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "index", - type: "uint256", - }, - { - internalType: "address", - name: "account", - type: "address", - }, - { - internalType: "uint256", - name: "amount", - type: "uint256", - }, - { - internalType: "bytes32[]", - name: "merkleProof", - type: "bytes32[]", - }, - { - internalType: "address", - name: "communityRef", - type: "address", - }, - ], - name: "claimWithCommunityCode", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [], - name: "clawBack", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [], - name: "communityBonusMultiplier", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "endTime", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "index", - type: "uint256", - }, - ], - name: "isClaimed", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "merkleRoot", - outputs: [ - { - internalType: "bytes32", - name: "", - type: "bytes32", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "owner", - outputs: [ - { - internalType: "address", - name: "", - type: "address", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "renounceOwnership", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [], - name: "startTime", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "token", - outputs: [ - { - internalType: "address", - name: "", - type: "address", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "newOwner", - type: "address", - }, - ], - name: "transferOwnership", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, -] as const - -export const ClaimWithFriends = new ethers.Contract( - VOTE_WITH_FRIENDS_ADDRESS, // "** address from calling DoggoDeployer.VOTE_WITH_FRIENDS(), this should probably be a function **", - CLAIM_WITH_FRIENDS_ABI, -) diff --git a/background/services/index.ts b/background/services/index.ts index d3d7097775..8572a12285 100644 --- a/background/services/index.ts +++ b/background/services/index.ts @@ -15,7 +15,7 @@ export { default as NameService } from "./name" export { default as PreferenceService } from "./preferences" export { default as ProviderBridgeService } from "./provider-bridge" export { default as InternalEthereumProviderService } from "./internal-ethereum-provider" -export { default as DoggoService } from "./doggo" +export { default as IslandService } from "./island" export { default as TelemetryService } from "./telemetry" export { default as LedgerService } from "./ledger" export { default as SigningService } from "./signing" diff --git a/background/services/indexing/db.ts b/background/services/indexing/db.ts index 27f3e6ad33..e6b23f5b0b 100644 --- a/background/services/indexing/db.ts +++ b/background/services/indexing/db.ts @@ -143,7 +143,10 @@ export class IndexingDatabase extends Dexie { * Tokens whose balances should be checked periodically. It might make sense * for this to be tracked against particular accounts in the future. */ - private assetsToTrack!: Dexie.Table + private assetsToTrack!: Dexie.Table< + SmartContractFungibleAsset, + [string, string] + > constructor(options?: DexieOptions) { super("tally/indexing", options) diff --git a/background/services/indexing/index.ts b/background/services/indexing/index.ts index af09e63fcc..128371d066 100644 --- a/background/services/indexing/index.ts +++ b/background/services/indexing/index.ts @@ -242,6 +242,15 @@ export default class IndexingService extends BaseService { await this.db.addAssetToTrack(asset) } + /** + * Check whether the specified asset is already being tracked. + * + * @param asset The fungible asset to track. + */ + async isTrackingAsset(asset: SmartContractFungibleAsset): Promise { + return this.db.isTrackingAsset(asset) + } + /** * Adds/updates a custom asset, invalidates internal cache for asset network * @param asset The custom asset @@ -283,6 +292,13 @@ export default class IndexingService extends BaseService { * lists. */ async cacheAssetsForNetwork(network: EVMNetwork): Promise { + // FIXME Somewhere along the line, we started confusing tracked and custom + // FIXME assets as informational data. We pull tracked and then custom + // FIXME assets, but really this should never touch custom assets; all + // FIXME custom assets should be tracked if we want to pull them. + const trackedAssets = (await this.db.getAssetsToTrack()).filter((asset) => + sameNetwork(asset.homeNetwork, network), + ) const customAssets = await this.db.getActiveCustomAssetsByNetworks([ network, ]) @@ -292,6 +308,7 @@ export default class IndexingService extends BaseService { this.cachedAssets[network.chainID] = mergeAssets( [network.baseAsset], + trackedAssets, customAssets, networkAssetsFromLists(network, tokenLists), ) @@ -710,6 +727,7 @@ export default class IndexingService extends BaseService { [address: HexString]: HexString } verified?: boolean + logoURL?: string } = {}, ): Promise { const normalizedAddress = normalizeEVMAddress(contractAddress) @@ -750,9 +768,11 @@ export default class IndexingService extends BaseService { if (!isRemoved || (isRemoved && isVerified)) { if (Object.keys(metadata).length !== 0) { customAsset.metadata ??= {} + if (metadata.verified !== undefined) { customAsset.metadata.verified = metadata.verified } + if (metadata.discoveryTxHash) { customAsset.metadata.discoveryTxHash ??= {} Object.assign( @@ -761,6 +781,10 @@ export default class IndexingService extends BaseService { ) } + if (metadata.logoURL) { + customAsset.metadata.logoURL = metadata.logoURL + } + if (isRemoved) { customAsset.metadata.removed = false } diff --git a/background/services/island/contracts.ts b/background/services/island/contracts.ts new file mode 100644 index 0000000000..ed39a7e22b --- /dev/null +++ b/background/services/island/contracts.ts @@ -0,0 +1,1955 @@ +import browser from "webextension-polyfill" +import { ethers } from "ethers" +import { SmartContractFungibleAsset } from "../../assets" +import { WEBSITE_ORIGIN } from "../../constants/website" +import { ARBITRUM_SEPOLIA } from "../../constants" +import { NormalizedEVMAddress } from "../../types" + +export const ISLAND_NETWORK = ARBITRUM_SEPOLIA // TODO: change once we move to Arbitrum One + +export const VOTE_WITH_FRIENDS_ADDRESS = + "0x0036B3a9D385Ce2CC072cf4A26dE29aE3283DEd0" + +export const TESTNET_TAHO: SmartContractFungibleAsset = { + name: "Taho", + symbol: "TAHO", + decimals: 18, + contractAddress: + process.env.TESTNET_TAHO_ADDRESS ?? ethers.constants.AddressZero, + homeNetwork: ISLAND_NETWORK, + metadata: { + tokenLists: [], + websiteURL: WEBSITE_ORIGIN, + logoURL: browser.runtime.getURL("images/assets/doggo.png"), + verified: true, + }, +} + +export const TESTNET_TAHO_DEPLOYER_ADDRESS = + process.env.TESTNET_TAHO_DEPLOYER_ADDRESS ?? ethers.constants.AddressZero + +const TESTNET_REALM_ABI = [ + { + inputs: [ + { + internalType: "string", + name: "realmName", + type: "string", + }, + { + internalType: "string", + name: "_xpTokenNamePrefix", + type: "string", + }, + { + internalType: "string", + name: "_xpTokenSymbolPrefix", + type: "string", + }, + { + internalType: "contract GameParameters", + name: "_gameParameters", + type: "address", + }, + { + internalType: "contract Game", + name: "_game", + type: "address", + }, + { + internalType: "contract Taho", + name: "_taho", + type: "address", + }, + { + internalType: "contract VeTaho", + name: "_veTaho", + type: "address", + }, + { + internalType: "address", + name: "initialCouncil", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [ + { + internalType: "uint256", + name: "actual", + type: "uint256", + }, + { + internalType: "uint256", + name: "required", + type: "uint256", + }, + ], + name: "NotEnoughSignatures", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "oldCouncilAddress", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newCouncilAddress", + type: "address", + }, + ], + name: "CouncilAddressUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address[]", + name: "oldMembers", + type: "address[]", + }, + { + indexed: false, + internalType: "address[]", + name: "newMembers", + type: "address[]", + }, + ], + name: "CouncilMembersUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "string", + name: "newName", + type: "string", + }, + ], + name: "NameUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "string", + name: "newQuestlineUrl", + type: "string", + }, + ], + name: "QuestlineUrlUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "staker", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "unlockAt", + type: "uint256", + }, + ], + name: "Staked", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "staker", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "Unstaked", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "newVersion", + type: "address", + }, + ], + name: "UpgradeFinalized", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "newVersion", + type: "address", + }, + ], + name: "UpgradeInitiated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "season", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "xp", + type: "address", + }, + ], + name: "XpCreated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "xp", + type: "address", + }, + { + indexed: false, + internalType: "string", + name: "merkleDataUrl", + type: "string", + }, + { + indexed: false, + internalType: "bytes32", + name: "merkleRoot", + type: "bytes32", + }, + { + indexed: false, + internalType: "address", + name: "distributor", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "XpDistributed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "bytes32", + name: "newXpFunctionHash", + type: "bytes32", + }, + ], + name: "XpFunctionUpdated", + type: "event", + }, + { + inputs: [], + name: "DOMAIN_SEPARATOR", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "UPDATE_COUNCIL_TYPEHASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "cachedChainId", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "cachedDomainSeparator", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "newXpFunctionHash", + type: "bytes32", + }, + ], + name: "commitXpFunction", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "council", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + name: "councilMembers", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "createXp", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "merkleRoot", + type: "bytes32", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "string", + name: "merkleDataUrl", + type: "string", + }, + ], + name: "distributeXp", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "finalizeUpgrade", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "game", + outputs: [ + { + internalType: "contract Game", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "gameParameters", + outputs: [ + { + internalType: "contract GameParameters", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getCouncilMembers", + outputs: [ + { + internalType: "address[]", + name: "", + type: "address[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_newVersion", + type: "address", + }, + ], + name: "initiateUpgrade", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "name", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "newVersion", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + name: "nonces", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "questlineUrl", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "typeHashDigest", + type: "bytes32", + }, + { + internalType: "uint8", + name: "v", + type: "uint8", + }, + { + internalType: "bytes32", + name: "r", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "s", + type: "bytes32", + }, + ], + name: "recoverFromTypeHashSignature", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address[]", + name: "newCouncilMembers", + type: "address[]", + }, + ], + name: "setCouncilMembers", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "stake", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + name: "stakeUnlockTime", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "taho", + outputs: [ + { + internalType: "contract Taho", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "unstake", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newCouncil", + type: "address", + }, + { + internalType: "bytes", + name: "signatures", + type: "bytes", + }, + ], + name: "updateCouncilAddress", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "newName", + type: "string", + }, + ], + name: "updateName", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "newQuestlineUrl", + type: "string", + }, + ], + name: "updateQuestlineUrl", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "veTaho", + outputs: [ + { + internalType: "contract VeTaho", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "xp", + outputs: [ + { + internalType: "contract Xp", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "xpFunctionHash", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "xpTokenNamePrefix", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "xpTokenSymbolPrefix", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, +] + +const TESTNET_TAHO_DEPLOYER_ABI = [ + { + inputs: [], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "deployHash", + type: "bytes32", + }, + ], + name: "UnknownContract", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "contractAddress", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "deployerAddress", + type: "address", + }, + ], + name: "Deployed", + type: "event", + }, + { + inputs: [], + name: "APPROVAL_TARGET", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "APPROVAL_TARGET_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "BALANCER_POOL", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "BALANCER_POOL_AGENT", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "BALANCER_POOL_AGENT_DEPLOYER_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "BALANCER_POOL_CANDIDATE", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "BALANCER_POOL_DEPLOYER_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "CLAIM_WITH_FRIENDS", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "CLAIM_WITH_FRIENDS_DEPLOYER_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "CREATORS_REALM", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "CREATORS_REALM_DEPLOYER_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "CREATORS_REALM_VETAHO", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "CREATORS_REALM_VETAHO_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "DEFI_REALM", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "DEFI_REALM_DEPLOYER_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "DEFI_REALM_VETAHO", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "DEFI_REALM_VETAHO_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "DEV_GRANT_ESCROW", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "DEV_GRANT_ESCROW_DEPLOYER_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "DEV_TEAM_MULTISIG", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "EDUCATE_REALM", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "EDUCATE_REALM_DEPLOYER_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "EDUCATE_REALM_VETAHO", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "EDUCATE_REALM_VETAHO_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "ELECTIONS", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "ELECTIONS_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "EXPIRING_MERKLE_DISTRIBUTOR_FACTORY", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "GAME", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "GAME_DEPLOYER_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "GAME_PARAMETERS", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "GAME_PARAMETERS_DEPLOYER_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "HUNTING_GROUND_REGISTRY", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "HUNTING_GROUND_REGISTRY_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "HUNTING_GROUND_REWARDS_ESCROW", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "HUNTING_GROUND_REWARDS_ESCROW_DEPLOYER_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "MERKLE_FACTORY_DEPLOYER_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "SOCIAL_REALM", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "SOCIAL_REALM_DEPLOYER_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "SOCIAL_REALM_VETAHO", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "SOCIAL_REALM_VETAHO_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "TAHO_COMMUNITY_MULTISIG", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "TAHO_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "TAHO_ETH_HUNTING_GROUND", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "TAHO_ETH_HUNTING_GROUND_DEPLOYER_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "TAHO_SALT", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "TAHO_TOKEN", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "TREASURY_VESTER", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "TREASURY_VESTER_DEPLOYER_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "VAMPIRE_REALM", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "VAMPIRE_REALM_DEPLOYER_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "VAMPIRE_REALM_VETAHO", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "VAMPIRE_REALM_VETAHO_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "XP", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "XP_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "XP_FACTORY", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "XP_FACTORY_DEPLOYER_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "XP_MERKLE_DISTRIBUTOR_FACTORY", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "XP_MERKLE_DISTRIBUTOR_FACTORY_DEPLOYER_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "XP_TAHO_CONVERTER_FACTORY", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "XP_TAHO_CONVERTER_FACTORY_DEPLOYER_DEPLOY_HASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + name: "contractRewards", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "contractsPendingDeploymentCount", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes", + name: "_initCode", + type: "bytes", + }, + ], + name: "deploy", + outputs: [ + { + internalType: "address payable", + name: "createdContract", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "finalize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "balancerPoolCandidate", + type: "address", + }, + ], + name: "setBalancerPoolCandidate", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const + +export const CLAIM_WITH_FRIENDS_ABI = [ + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "index", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "account", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "Claimed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "index", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "claimant", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amountClaimed", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "claimedBonus", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "communityRef", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "communityBonus", + type: "uint256", + }, + ], + name: "ClaimedWithCommunityCode", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "previousOwner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "OwnershipTransferred", + type: "event", + }, + { + inputs: [ + { + internalType: "uint256", + name: "index", + type: "uint256", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "bytes32[]", + name: "merkleProof", + type: "bytes32[]", + }, + ], + name: "claim", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "index", + type: "uint256", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "bytes32[]", + name: "merkleProof", + type: "bytes32[]", + }, + { + internalType: "address", + name: "communityRef", + type: "address", + }, + ], + name: "claimWithCommunityCode", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "clawBack", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "communityBonusMultiplier", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "endTime", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "index", + type: "uint256", + }, + ], + name: "isClaimed", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "merkleRoot", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "renounceOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "startTime", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "token", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "transferOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const + +export const STARTING_REALM_NAMES = [ + "Creators", + "Defi", + "Educate", + "Social", + "Vampire", +] + +export function buildRealmContract( + address: NormalizedEVMAddress, +): ethers.Contract { + return new ethers.Contract(address, TESTNET_REALM_ABI) +} + +export const ClaimWithFriends = new ethers.Contract( + VOTE_WITH_FRIENDS_ADDRESS, // "** address from calling DoggoDeployer.VOTE_WITH_FRIENDS(), this should probably be a function **", + CLAIM_WITH_FRIENDS_ABI, +) + +export const TestnetTahoDeployer = new ethers.Contract( + TESTNET_TAHO_DEPLOYER_ADDRESS, + TESTNET_TAHO_DEPLOYER_ABI, +) diff --git a/background/services/doggo/db.ts b/background/services/island/db.ts similarity index 84% rename from background/services/doggo/db.ts rename to background/services/island/db.ts index c103101833..e9b41af1b0 100644 --- a/background/services/doggo/db.ts +++ b/background/services/island/db.ts @@ -1,20 +1,16 @@ import Dexie from "dexie" import { AddressOnNetwork } from "../../accounts" import { normalizeEVMAddress } from "../../lib/utils" +import { ReferrerStats } from "./types" -export type ReferrerStats = { - bonusTotal: bigint - referredUsers: number -} - -export class DoggoDatabase extends Dexie { +export class IslandDatabase extends Dexie { private referralBonuses!: Dexie.Table< AddressOnNetwork & { referredBy: AddressOnNetwork; referralBonus: bigint }, [string, string, string] > constructor() { - super("tally/doggo") + super("taho/island") this.version(1).stores({ referralBonuses: @@ -54,6 +50,6 @@ export class DoggoDatabase extends Dexie { } } -export async function getOrCreateDB(): Promise { - return new DoggoDatabase() +export async function getOrCreateDB(): Promise { + return new IslandDatabase() } diff --git a/background/services/doggo/defaults.ts b/background/services/island/defaults.ts similarity index 100% rename from background/services/doggo/defaults.ts rename to background/services/island/defaults.ts diff --git a/background/services/doggo/index.ts b/background/services/island/index.ts similarity index 56% rename from background/services/doggo/index.ts rename to background/services/island/index.ts index 233d7b03de..8a3f8604d5 100644 --- a/background/services/doggo/index.ts +++ b/background/services/island/index.ts @@ -1,60 +1,169 @@ -import { BigNumber } from "ethers" +import { BigNumber, ethers } from "ethers" import { ServiceLifecycleEvents, ServiceCreatorFunction } from "../types" -import { Eligible } from "./types" +import { Eligible, ReferrerStats } from "./types" import BaseService from "../base" import { getFileHashProspect, getClaimFromFileHash } from "./utils" import ChainService from "../chain" import { DOGGO, ETHEREUM } from "../../constants" import { sameNetwork } from "../../networks" -import { ClaimWithFriends } from "./contracts" +import { + ClaimWithFriends, + ISLAND_NETWORK, + STARTING_REALM_NAMES, + TESTNET_TAHO, + TestnetTahoDeployer, + buildRealmContract, +} from "./contracts" import IndexingService from "../indexing" import { initialVaults } from "../../redux-slices/earn" import logger from "../../lib/logger" import { HexString } from "../../types" import { AddressOnNetwork } from "../../accounts" -import { DoggoDatabase, getOrCreateDB, ReferrerStats } from "./db" +import { IslandDatabase, getOrCreateDB } from "./db" import { normalizeEVMAddress } from "../../lib/utils" -import { FeatureFlags, isEnabled } from "../../features" +import { FeatureFlags, isDisabled, isEnabled } from "../../features" +import { SmartContractFungibleAsset } from "../../assets" + +export { + TESTNET_TAHO, + VOTE_WITH_FRIENDS_ADDRESS, + TestnetTahoDeployer as TahoDeployer, +} from "./contracts" + +export { ReferrerStats } from "./types" interface Events extends ServiceLifecycleEvents { newEligibility: Eligible newReferral: { referrer: AddressOnNetwork } & ReferrerStats + monitoringTestnetAsset: SmartContractFungibleAsset } /* - * The DOGGO service handles interactions, caching, and indexing related to the - * DOGGO token and its capabilities. + * The Island service handles interactions, caching, and indexing related to + * the Island game and its capabilities. * - * This includes handling DOGGO claim data, as well as + * This includes handling Island contracts, as well as metadata that is best + * maintained in-wallet around XP, etc. */ -export default class DoggoService extends BaseService { +export default class IslandService extends BaseService { + private isRelevantMonitoringAlreadyEnabled = false + static create: ServiceCreatorFunction< Events, - DoggoService, + IslandService, [Promise, Promise] > = async (chainService, indexingService) => new this(await getOrCreateDB(), await chainService, await indexingService) private constructor( - private db: DoggoDatabase, + private db: IslandDatabase, private chainService: ChainService, private indexingService: IndexingService, ) { - super() + super({ + startMonitoringIfNeeded: { + schedule: { + periodInMinutes: 10, + }, + handler: () => this.startMonitoringIfNeeded(), + }, + }) } - protected override async internalStartService(): Promise { - await super.internalStartService() + async startMonitoringIfNeeded(): Promise { + if (isDisabled(FeatureFlags.SUPPORT_THE_ISLAND)) { + logger.debug("Island testnet disabled, not setting up The Island...") + this.isRelevantMonitoringAlreadyEnabled = true + return + } - const huntingGrounds = initialVaults + if (this.isRelevantMonitoringAlreadyEnabled) { + return + } + + const islandProvider = this.chainService.providerForNetwork(ISLAND_NETWORK) + if (islandProvider === undefined) { + logger.debug( + "No Arbitrum provider available, not setting up The Island...", + ) + + return + } + + this.chainService.startTrackingNetworkOrThrow(ISLAND_NETWORK.chainID) + + try { + // Bail if the TAHO contract hasn't been deployed. + if ( + (await islandProvider.getCode(TESTNET_TAHO.contractAddress)) === "0x" + ) { + logger.debug("TAHO contract not deployed, not setting up The Island...") + return + } + + if (!(await this.indexingService.isTrackingAsset(TESTNET_TAHO))) { + await this.indexingService.addAssetToTrack(TESTNET_TAHO) + + this.emitter.emit("monitoringTestnetAsset", TESTNET_TAHO) + } + + const connectedDeployer = TestnetTahoDeployer.connect(islandProvider) + await Promise.all( + STARTING_REALM_NAMES.map(async (realmName) => { + const realmAddress = + await connectedDeployer.functions[ + `${realmName.toUpperCase()}_REALM` + ]() + const realmContract = buildRealmContract(realmAddress[0]).connect( + islandProvider, + ) + + const realmVeTahoAddress = (await realmContract.functions.veTaho())[0] + const realmVeAsset = + await this.indexingService.addTokenToTrackByContract( + ISLAND_NETWORK, + realmVeTahoAddress, + TESTNET_TAHO.metadata, + ) + if (realmVeAsset !== undefined) { + this.emitter.emit("monitoringTestnetAsset", realmVeAsset) + } + + const realmXpAddress = (await realmContract.functions.xp())[0] + + if (realmXpAddress === ethers.constants.AddressZero) { + logger.debug( + `XP token for realm ${realmName} at ${realmAddress} is not yet set, throwing an error to retry tracking later.`, + ) + + throw new Error(`XP token does not exist for realm ${realmAddress}`) + } - const ethereumProvider = this.chainService.providerForNetwork(ETHEREUM) - if (ethereumProvider === undefined) { - logger.error( - "No Ethereum provider available, not setting up DOGGO monitoring...", + const realmXpAsset = + await this.indexingService.addTokenToTrackByContract( + ISLAND_NETWORK, + realmXpAddress, + TESTNET_TAHO.metadata, + ) + if (realmXpAsset !== undefined) { + this.emitter.emit("monitoringTestnetAsset", realmXpAsset) + } + }), ) + } catch (error) { + logger.error("Error setting up monitoring for realms", error) + throw error } + // If all monitoring is enabled successfully, don't try again later. + this.isRelevantMonitoringAlreadyEnabled = true + } + + protected override async internalStartService(): Promise { + await super.internalStartService() + + const huntingGrounds = initialVaults + if (!isEnabled(FeatureFlags.HIDE_TOKEN_FEATURES)) { // Make sure the hunting ground assets are being tracked. huntingGrounds.forEach(({ network, asset }) => { diff --git a/background/services/doggo/types.ts b/background/services/island/types.ts similarity index 78% rename from background/services/doggo/types.ts rename to background/services/island/types.ts index 8f84aea037..7389f8254f 100644 --- a/background/services/doggo/types.ts +++ b/background/services/island/types.ts @@ -1,5 +1,10 @@ import { HexString } from "../../types" +export type ReferrerStats = { + bonusTotal: bigint + referredUsers: number +} + export interface Eligible { index: HexString account: HexString diff --git a/background/services/doggo/utils.ts b/background/services/island/utils.ts similarity index 100% rename from background/services/doggo/utils.ts rename to background/services/island/utils.ts diff --git a/background/services/preferences/db.ts b/background/services/preferences/db.ts index 4b902f0df8..d0687ffa5b 100644 --- a/background/services/preferences/db.ts +++ b/background/services/preferences/db.ts @@ -71,6 +71,7 @@ export type Preferences = { export type ManuallyDismissableItem = | "analytics-enabled-banner" | "copy-sensitive-material-warning" + | "testnet-portal-is-open-banner" /** * Items that the user will see once and will not be auto-displayed again. Can * be used for tours, or for popups that can be retriggered but will not diff --git a/ui/components/Wallet/Banner/PortalBanner.tsx b/ui/components/Wallet/Banner/PortalBanner.tsx new file mode 100644 index 0000000000..e891a2a6d4 --- /dev/null +++ b/ui/components/Wallet/Banner/PortalBanner.tsx @@ -0,0 +1,88 @@ +import browser from "webextension-polyfill" +import React, { ReactElement } from "react" +import { selectHasIslandAssets } from "@tallyho/tally-background/redux-slices/claim" +import { useBackgroundSelector } from "../../../hooks" +import SharedButton from "../../Shared/SharedButton" +import SharedIcon from "../../Shared/SharedIcon" +import SharedBanner from "../../Shared/SharedBanner" + +const PORTAL_ID_WITH_ASSETS = "testnet-portal-is-open-banner-with-assets" +const PORTAL_ID_NO_ASSETS = "testnet-portal-is-open-banner-no-assets" + +export default function PortalBanner(): ReactElement | null { + const hasIslandAssets = useBackgroundSelector(selectHasIslandAssets) + + const showIslandAndDismissBanner = () => { + browser.tabs.create({ url: "https://app.taho.xyz" }) + } + + return ( + + +

Subscape is online

+

+ {hasIslandAssets + ? "You're eligible for the Beta." + : "Try the live Beta."} +

+ + {hasIslandAssets ? "Explore Subscape" : "Check eligibility"} + + + +
+ ) +} diff --git a/ui/hooks/dom-hooks.ts b/ui/hooks/dom-hooks.ts index c45043b39e..ee094d0af3 100644 --- a/ui/hooks/dom-hooks.ts +++ b/ui/hooks/dom-hooks.ts @@ -130,10 +130,18 @@ export function useLocalStorage( const [value, setValue] = useState(() => getLocalStorageItem(key, initialValue), ) + const [cachedKey, setCachedKey] = useState(key) useEffect(() => { - setLocalStorageItem(key, value) - }, [key, value]) + if (key !== cachedKey) { + setValue(getLocalStorageItem(key, initialValue)) + setCachedKey(key) + } + }, [key, cachedKey, initialValue]) + + useEffect(() => { + setLocalStorageItem(cachedKey, value) + }, [cachedKey, value]) return [value, setValue] } diff --git a/ui/pages/Onboarding/Tabbed/ViewOnlyWallet.tsx b/ui/pages/Onboarding/Tabbed/ViewOnlyWallet.tsx index 3e1c2bc066..5703522666 100644 --- a/ui/pages/Onboarding/Tabbed/ViewOnlyWallet.tsx +++ b/ui/pages/Onboarding/Tabbed/ViewOnlyWallet.tsx @@ -6,6 +6,8 @@ import { HexString } from "@tallyho/tally-background/types" import { AddressOnNetwork } from "@tallyho/tally-background/accounts" import { selectCurrentAccount } from "@tallyho/tally-background/redux-slices/selectors" import { useTranslation } from "react-i18next" +import { FeatureFlags, isEnabled } from "@tallyho/tally-background/features" +import { ISLAND_NETWORK } from "@tallyho/tally-background/services/island/contracts" import { useBackgroundDispatch, useBackgroundSelector } from "../../../hooks" import SharedButton from "../../../components/Shared/SharedButton" import SharedAddressInput from "../../../components/Shared/SharedAddressInput" @@ -45,6 +47,18 @@ export default function ViewOnlyWallet(): ReactElement { } await dispatch(addAddressNetwork(addressOnNetwork)) + + // To show Island banner for read only accounts we have to dispatch it manually + // for additional network as read only accounts are added only to the current network + if (isEnabled(FeatureFlags.SUPPORT_THE_ISLAND)) { + await dispatch( + addAddressNetwork({ + address: addressOnNetwork.address, + network: ISLAND_NETWORK, + }), + ) + } + dispatch(setNewSelectedAccount(addressOnNetwork)) setRedirect(true) }, [dispatch, addressOnNetwork]) diff --git a/ui/pages/Wallet.tsx b/ui/pages/Wallet.tsx index a2a68d7d78..7bd7267935 100644 --- a/ui/pages/Wallet.tsx +++ b/ui/pages/Wallet.tsx @@ -26,6 +26,7 @@ import NFTListCurrentWallet from "../components/NFTs/NFTListCurrentWallet" import WalletHiddenAssets from "../components/Wallet/WalletHiddenAssets" import SharedButton from "../components/Shared/SharedButton" import SharedIcon from "../components/Shared/SharedIcon" +import PortalBanner from "../components/Wallet/Banner/PortalBanner" export default function Wallet(): ReactElement { const { t } = useTranslation() @@ -113,6 +114,7 @@ export default function Wallet(): ReactElement { {!isEnabled(FeatureFlags.HIDE_TOKEN_FEATURES) && ( )} +