diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 31d0114c2..0f35a4bf0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -42,7 +42,7 @@ jobs:
env:
NODE_ENV: production
DEV_RPC: http://localhost:8545
- FORK_URL: https://gnosis-pokt.nodies.app
+ FORK_URL: https://rpc-gate.autonolas.tech/gnosis-rpc/
- run: rm -rf /dist
@@ -55,7 +55,7 @@ jobs:
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
CSC_LINK: ${{ secrets.CSC_LINK }}
DEV_RPC: http://localhost:8545
- FORK_URL: https://gnosis-pokt.nodies.app
+ FORK_URL: https://rpc-gate.autonolas.tech/gnosis-rpc/
GH_TOKEN: ${{ secrets.github_token}}
NODE_ENV: production
#PUBLISH_FOR_PULL_REQUEST: true #required during testing
diff --git a/frontend/components/Main/MainAddFunds.tsx b/frontend/components/Main/MainAddFunds.tsx
index c955f1dc7..599e809b6 100644
--- a/frontend/components/Main/MainAddFunds.tsx
+++ b/frontend/components/Main/MainAddFunds.tsx
@@ -20,7 +20,7 @@ import styled from 'styled-components';
import { copyToClipboard, truncateAddress } from '@/common-util';
import { COLOR, COW_SWAP_GNOSIS_XDAI_OLAS_URL } from '@/constants';
import { UNICODE_SYMBOLS } from '@/constants/unicode';
-import { useBalance } from '@/hooks';
+import { useWallet } from '@/hooks/useWallet';
import { Address } from '@/types';
import { CardSection } from '../styled/CardSection';
@@ -34,22 +34,24 @@ const CustomizedCardSection = styled(CardSection)<{ border?: boolean }>`
`;
export const MainAddFunds = () => {
- const { wallets } = useBalance();
+ const { masterSafeAddress, masterEoaAddress } = useWallet();
const [isAddFundsVisible, setIsAddFundsVisible] = useState(false);
- const walletAddress = useMemo(() => wallets[0]?.address, [wallets]);
+ const fundingAddress: Address | undefined =
+ masterSafeAddress ?? masterEoaAddress;
- const truncatedWalletAddress = useMemo(
- () => truncateAddress(walletAddress),
- [walletAddress],
+ const truncatedFundingAddress: string | undefined = useMemo(
+ () => fundingAddress && truncateAddress(fundingAddress),
+ [fundingAddress],
);
- const handleCopyWalletAddress = useCallback(
+ const handleCopyAddress = useCallback(
() =>
- copyToClipboard(walletAddress).then(() =>
+ fundingAddress &&
+ copyToClipboard(fundingAddress).then(() =>
message.success('Copied successfully!'),
),
- [walletAddress],
+ [fundingAddress],
);
return (
@@ -78,9 +80,9 @@ export const MainAddFunds = () => {
<>
>
@@ -111,19 +113,23 @@ const AddFundsWarningAlertSection = () => (
);
const AddFundsAddressSection = ({
- walletAddress,
- truncatedWalletAddress,
+ fundingAddress,
+ truncatedFundingAddress,
handleCopy,
}: {
- walletAddress: Address;
- truncatedWalletAddress: string;
+ fundingAddress?: string;
+ truncatedFundingAddress?: string;
handleCopy: () => void;
}) => (
{walletAddress}}
+ title={
+
+ {fundingAddress ?? 'Error loading address'}
+
+ }
>
- GNO: {truncatedWalletAddress}
+ GNO: {truncatedFundingAddress ?? '--'}
-
- Backup wallet
- gotoSettings(SettingsScreen.AddBackupWallet)}
- >
- Add backup wallet
-
+
+ Backup wallet
+ {backupSafeAddress ? (
+
+ {truncatedBackupSafeAddress} {UNICODE_SYMBOLS.EXTERNAL_LINK}
+
+ ) : (
+
+ )}
);
};
+
+const NoBackupWallet = () => {
+ const { goto: gotoSettings } = useSettings();
+ return (
+ <>
+
+ No backup wallet added.
+
+
+
+
+
+ Your funds are at risk!
+
+
+ You will lose any assets you send on other chains.
+
+
+ >
+ }
+ />
+
+ gotoSettings(SettingsScreen.AddBackupWallet)}
+ >
+ Add backup wallet
+
+ >
+ );
+};
diff --git a/frontend/components/Setup/Create/SetupEoaFunding.tsx b/frontend/components/Setup/Create/SetupEoaFunding.tsx
index d0e1568e8..1ae362b71 100644
--- a/frontend/components/Setup/Create/SetupEoaFunding.tsx
+++ b/frontend/components/Setup/Create/SetupEoaFunding.tsx
@@ -16,17 +16,20 @@ import { Chain } from '@/client';
import { copyToClipboard } from '@/common-util';
import { CardFlex } from '@/components/styled/CardFlex';
import { CardSection } from '@/components/styled/CardSection';
-import { COLOR, COW_SWAP_GNOSIS_XDAI_OLAS_URL } from '@/constants';
+import {
+ COLOR,
+ COW_SWAP_GNOSIS_XDAI_OLAS_URL,
+ MIN_ETH_BALANCE_THRESHOLDS,
+} from '@/constants';
import { UNICODE_SYMBOLS } from '@/constants/unicode';
import { PageState, SetupScreen } from '@/enums';
import { useBalance, usePageState, useSetup } from '@/hooks';
+import { useWallet } from '@/hooks/useWallet';
import { WalletService } from '@/service/Wallet';
import { Address } from '@/types';
import { SetupCreateHeader } from './SetupCreateHeader';
-const MASTER_EAO_FUNDING_AMOUNT_ETH = 0.1;
-
enum SetupEaoFundingStatus {
WaitingForEoaFunding,
CreatingSafe,
@@ -40,26 +43,27 @@ const loadingStatuses = [
];
export const SetupEoaFunding = () => {
- const { wallets, walletBalances } = useBalance();
+ const { masterEoaAddress: masterEaoAddress, masterSafeAddress } = useWallet();
+ const { walletBalances } = useBalance();
const { backupSigner } = useSetup();
const { goto } = usePageState();
const [isCreatingSafe, setIsCreatingSafe] = useState(false);
- const masterEoa = wallets?.[0]?.address;
- const masterEaoEthBalance = walletBalances?.[masterEoa]?.ETH;
-
- const masterSafe = wallets?.[0]?.safe;
+ const masterEaoEthBalance =
+ masterEaoAddress && walletBalances?.[masterEaoAddress]?.ETH;
const isFundedMasterEoa =
- masterEaoEthBalance && masterEaoEthBalance >= MASTER_EAO_FUNDING_AMOUNT_ETH;
+ masterEaoEthBalance &&
+ masterEaoEthBalance >=
+ MIN_ETH_BALANCE_THRESHOLDS[Chain.GNOSIS].safeCreation;
const status = useMemo(() => {
if (!isFundedMasterEoa) return SetupEaoFundingStatus.WaitingForEoaFunding;
if (isCreatingSafe) return SetupEaoFundingStatus.CreatingSafe;
- if (masterSafe) return SetupEaoFundingStatus.Done;
+ if (masterSafeAddress) return SetupEaoFundingStatus.Done;
return SetupEaoFundingStatus.Error;
- }, [isCreatingSafe, isFundedMasterEoa, masterSafe]);
+ }, [isCreatingSafe, isFundedMasterEoa, masterSafeAddress]);
const statusMessage = useMemo(() => {
switch (status) {
@@ -81,7 +85,7 @@ export const SetupEoaFunding = () => {
setIsCreatingSafe(true);
message.success('Funds have been received!');
// TODO: add backup signer
- WalletService.createSafe(Chain.GNOSIS).catch((e) => {
+ WalletService.createSafe(Chain.GNOSIS, backupSigner).catch((e) => {
console.error(e);
message.error('Failed to create an account. Please try again later.');
});
@@ -89,8 +93,8 @@ export const SetupEoaFunding = () => {
useEffect(() => {
// Only progress is the safe is created and accessible via context (updates on interval)
- if (masterSafe) goto(PageState.Main);
- }, [goto, masterSafe]);
+ if (masterSafeAddress) goto(PageState.Main);
+ }, [goto, masterSafeAddress]);
return (
@@ -99,7 +103,7 @@ export const SetupEoaFunding = () => {
disabled={isCreatingSafe}
/>
- Deposit {MASTER_EAO_FUNDING_AMOUNT_ETH} XDAI
+ Deposit {MIN_ETH_BALANCE_THRESHOLDS[Chain.GNOSIS].safeCreation} XDAI
The app needs these funds to create your account on-chain.
@@ -115,7 +119,9 @@ export const SetupEoaFunding = () => {
Status: {statusMessage}
- {!isFundedMasterEoa && }
+ {!isFundedMasterEoa && (
+
+ )}
);
};
diff --git a/frontend/constants/thresholds.ts b/frontend/constants/thresholds.ts
index 9a24038bc..81e577534 100644
--- a/frontend/constants/thresholds.ts
+++ b/frontend/constants/thresholds.ts
@@ -2,7 +2,7 @@ import { Chain } from '@/client';
export const MIN_ETH_BALANCE_THRESHOLDS = {
[Chain.GNOSIS]: {
- safeCreation: 0.1,
+ safeCreation: 1.5,
safeAddSigner: 0.1,
},
};
diff --git a/frontend/context/BalanceProvider.tsx b/frontend/context/BalanceProvider.tsx
index 1c45694f3..26234ee2f 100644
--- a/frontend/context/BalanceProvider.tsx
+++ b/frontend/context/BalanceProvider.tsx
@@ -23,7 +23,6 @@ import { TOKENS } from '@/constants/tokens';
import { Token } from '@/enums/Token';
import { EthersService } from '@/service';
import MulticallService from '@/service/Multicall';
-import { WalletService } from '@/service/Wallet';
import {
Address,
AddressNumberRecord,
@@ -32,6 +31,7 @@ import {
import { ServicesContext } from '.';
import { RewardContext } from './RewardProvider';
+import { WalletContext } from './WalletProvider';
export const BalanceContext = createContext<{
isLoaded: boolean;
@@ -41,7 +41,7 @@ export const BalanceContext = createContext<{
olasDepositBalance?: number;
totalEthBalance?: number;
totalOlasBalance?: number;
- wallets: Wallet[];
+ wallets?: Wallet[];
walletBalances: WalletAddressNumberRecord;
updateBalances: () => Promise;
setIsPaused: Dispatch>;
@@ -53,13 +53,15 @@ export const BalanceContext = createContext<{
olasDepositBalance: undefined,
totalEthBalance: undefined,
totalOlasBalance: undefined,
- wallets: [],
+ wallets: undefined,
walletBalances: {},
updateBalances: async () => {},
setIsPaused: () => {},
});
export const BalanceProvider = ({ children }: PropsWithChildren) => {
+ const { wallets, masterEoaAddress, masterSafeAddress } =
+ useContext(WalletContext);
const { services, serviceAddresses } = useContext(ServicesContext);
const { optimisticRewardsEarnedForEpoch } = useContext(RewardContext);
@@ -68,7 +70,6 @@ export const BalanceProvider = ({ children }: PropsWithChildren) => {
const [olasDepositBalance, setOlasDepositBalance] = useState();
const [olasBondBalance, setOlasBondBalance] = useState();
const [isBalanceLoaded, setIsBalanceLoaded] = useState(false);
- const [wallets, setWallets] = useState([]);
const [walletBalances, setWalletBalances] =
useState({});
@@ -105,32 +106,32 @@ export const BalanceProvider = ({ children }: PropsWithChildren) => {
const updateBalances = useCallback(async (): Promise => {
try {
- const wallets = await getWallets();
- if (!wallets) return;
-
- setWallets(wallets);
-
- const walletAddresses = getWalletAddresses(wallets, serviceAddresses);
+ const walletAddresses: Address[] = [];
+ if (masterEoaAddress) walletAddresses.push(masterEoaAddress);
+ if (masterSafeAddress) walletAddresses.push(masterSafeAddress);
+ if (serviceAddresses) walletAddresses.push(...serviceAddresses);
const walletBalances = await getWalletBalances(walletAddresses);
if (!walletBalances) return;
setWalletBalances(walletBalances);
const serviceId = services?.[0]?.chain_data.token;
+
if (!isNumber(serviceId)) {
setIsLoaded(true);
setIsBalanceLoaded(true);
return;
}
- const serviceRegistryBalances = await getServiceRegistryBalances(
- wallets[0].address,
- serviceId,
- );
+ if (masterSafeAddress && serviceId) {
+ const serviceRegistryBalances = await getServiceRegistryBalances(
+ masterSafeAddress,
+ serviceId,
+ );
- // update olas balances
- setOlasDepositBalance(serviceRegistryBalances.depositValue);
- setOlasBondBalance(serviceRegistryBalances.bondValue);
+ setOlasDepositBalance(serviceRegistryBalances.depositValue);
+ setOlasBondBalance(serviceRegistryBalances.bondValue);
+ }
// update balance loaded state
setIsLoaded(true);
@@ -140,7 +141,7 @@ export const BalanceProvider = ({ children }: PropsWithChildren) => {
message.error('Unable to retrieve wallet balances');
setIsBalanceLoaded(true);
}
- }, [serviceAddresses, services]);
+ }, [masterEoaAddress, masterSafeAddress, serviceAddresses, services]);
useInterval(
() => {
@@ -195,9 +196,6 @@ export const getOlasBalances = async (
return olasBalances;
};
-export const getWallets = async (): Promise =>
- WalletService.getWallets();
-
export const getWalletAddresses = (
wallets: Wallet[],
serviceAddresses: Address[],
diff --git a/frontend/context/MasterSafeProvider.tsx b/frontend/context/MasterSafeProvider.tsx
index 5f364da95..9bd27e393 100644
--- a/frontend/context/MasterSafeProvider.tsx
+++ b/frontend/context/MasterSafeProvider.tsx
@@ -9,43 +9,32 @@ import {
useMemo,
useState,
} from 'react';
+import { useInterval } from 'usehooks-ts';
import { GnosisSafeService } from '@/service/GnosisSafe';
import { Address } from '@/types';
-import { BalanceContext } from '.';
+import { WalletContext } from './WalletProvider';
export const MasterSafeContext = createContext<{
backupSafeAddress?: Address;
masterSafeAddress?: Address;
masterEoaAddress?: Address;
masterSafeOwners?: Address[];
- updateOwners?: () => Promise;
+ updateMasterSafeOwners?: () => Promise;
}>({
backupSafeAddress: undefined,
masterSafeAddress: undefined,
masterEoaAddress: undefined,
masterSafeOwners: undefined,
- updateOwners: async () => {},
+ updateMasterSafeOwners: async () => {},
});
export const MasterSafeProvider = ({ children }: PropsWithChildren) => {
- const { wallets } = useContext(BalanceContext);
+ const { masterSafeAddress, masterEoaAddress } = useContext(WalletContext);
const [masterSafeOwners, setMasterSafeOwners] = useState();
- const masterSafeAddress = useMemo(() => {
- if (!wallets) return;
- if (!wallets.length) return;
- return wallets[0].safe;
- }, [wallets]);
-
- const masterEoaAddress = useMemo(() => {
- if (!wallets) return;
- if (!wallets.length) return;
- return wallets[0].address;
- }, [wallets]);
-
const backupSafeAddress = useMemo(() => {
if (!masterEoaAddress) return;
if (!masterSafeOwners) return;
@@ -62,22 +51,31 @@ export const MasterSafeProvider = ({ children }: PropsWithChildren) => {
return currentBackupAddress;
}, [masterEoaAddress, masterSafeOwners]);
- const updateOwners = async () => {
+ const updateMasterSafeOwners = async () => {
if (!masterSafeAddress) return;
- const safeSigners = await GnosisSafeService.getOwners({
- address: masterSafeAddress,
- });
- setMasterSafeOwners(safeSigners);
+ try {
+ const safeSigners = await GnosisSafeService.getOwners({
+ address: masterSafeAddress,
+ });
+ if (!safeSigners) return;
+ setMasterSafeOwners(safeSigners);
+ } catch (error) {
+ console.error('Error fetching safe owners', error);
+ }
};
+ useInterval(
+ updateMasterSafeOwners,
+ masterSafeOwners && masterSafeOwners.length >= 2 ? null : 5000,
+ );
+
return (
{children}
diff --git a/frontend/context/WalletProvider.tsx b/frontend/context/WalletProvider.tsx
new file mode 100644
index 000000000..533594308
--- /dev/null
+++ b/frontend/context/WalletProvider.tsx
@@ -0,0 +1,46 @@
+import { createContext, PropsWithChildren, useState } from 'react';
+import { useInterval } from 'usehooks-ts';
+
+import { Wallet } from '@/client';
+import { WalletService } from '@/service/Wallet';
+import { Address } from '@/types';
+
+export const WalletContext = createContext<{
+ masterEoaAddress?: Address;
+ masterSafeAddress?: Address;
+ wallets?: Wallet[];
+ updateWallets: () => Promise;
+}>({
+ masterEoaAddress: undefined,
+ masterSafeAddress: undefined,
+ wallets: undefined,
+ updateWallets: async () => {},
+});
+
+export const WalletProvider = ({ children }: PropsWithChildren) => {
+ const [wallets, setWallets] = useState();
+
+ const masterEoaAddress: Address | undefined = wallets?.[0]?.address;
+ const masterSafeAddress: Address | undefined = wallets?.[0]?.safe;
+
+ const updateWallets = async () => {
+ const wallets = await WalletService.getWallets();
+ if (!wallets) return;
+ setWallets(wallets);
+ };
+
+ useInterval(updateWallets, 5000);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/frontend/hooks/useMasterSafe.tsx b/frontend/hooks/useMasterSafe.tsx
new file mode 100644
index 000000000..ec5d52059
--- /dev/null
+++ b/frontend/hooks/useMasterSafe.tsx
@@ -0,0 +1,7 @@
+import { useContext } from 'react';
+
+import { MasterSafeContext } from '@/context/MasterSafeProvider';
+
+export const useMasterSafe = () => {
+ return useContext(MasterSafeContext);
+};
diff --git a/frontend/hooks/useWallet.tsx b/frontend/hooks/useWallet.tsx
new file mode 100644
index 000000000..be2b58b88
--- /dev/null
+++ b/frontend/hooks/useWallet.tsx
@@ -0,0 +1,15 @@
+import { useContext } from 'react';
+
+import { WalletContext } from '@/context/WalletProvider';
+
+export const useWallet = () => {
+ const { wallets, masterEoaAddress, masterSafeAddress, updateWallets } =
+ useContext(WalletContext);
+
+ return {
+ wallets,
+ masterEoaAddress,
+ masterSafeAddress,
+ updateWallets,
+ };
+};
diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx
index a2f40fe3b..3a8fc90f4 100644
--- a/frontend/pages/_app.tsx
+++ b/frontend/pages/_app.tsx
@@ -12,8 +12,10 @@ import {
SetupProvider,
} from '@/context';
import { BalanceProvider } from '@/context/BalanceProvider';
+import { MasterSafeProvider } from '@/context/MasterSafeProvider';
import { RewardProvider } from '@/context/RewardProvider';
import { SettingsProvider } from '@/context/SettingsProvider';
+import { WalletProvider } from '@/context/WalletProvider';
import { mainTheme } from '@/theme';
export default function App({ Component, pageProps }: AppProps) {
@@ -29,23 +31,27 @@ export default function App({ Component, pageProps }: AppProps) {
return (
-
-
-
-
-
- {isMounted ? (
-
-
-
-
-
- ) : null}
-
-
-
-
-
+
+
+
+
+
+
+
+ {isMounted ? (
+
+
+
+
+
+ ) : null}
+
+
+
+
+
+
+
);
diff --git a/operate/cli.py b/operate/cli.py
index b7dbb8487..6f90cad1b 100644
--- a/operate/cli.py
+++ b/operate/cli.py
@@ -90,6 +90,14 @@ def __init__(
)
self.password: t.Optional[str] = os.environ.get("OPERATE_USER_PASSWORD")
+ def create_user_account(self, password: str) -> UserAccount:
+ """Create a user account."""
+ self.password = password
+ return UserAccount.new(
+ password=password,
+ path=self._path / "user.json",
+ )
+
def service_manager(self) -> services.manage.ServiceManager:
"""Load service manager."""
return services.manage.ServiceManager(
@@ -248,9 +256,8 @@ async def _setup_account(request: Request) -> t.Dict:
)
data = await request.json()
- UserAccount.new(
+ operate.create_user_account(
password=data["password"],
- path=operate._path / "user.json", # pylint: disable=protected-access
)
return JSONResponse(content={"error": None})
@@ -417,6 +424,11 @@ async def _create_safe(request: Request) -> t.List[t.Dict]:
chain_type=chain_type,
owner=data.get("owner"),
)
+ wallet.transfer(
+ to=t.cast(str, wallet.safe),
+ amount=int(1e18),
+ chain_type=chain_type,
+ )
return JSONResponse(content={"safe": wallet.safe, "message": "Safe created!"})
@app.put("/api/wallet/safe")
@@ -485,6 +497,7 @@ async def _create_services(request: Request) -> JSONResponse:
on_chain_user_params=services.manage.OnChainUserParams.from_json(
template["configuration"]
),
+ from_safe=True,
)
update = True
else:
@@ -498,11 +511,9 @@ async def _create_services(request: Request) -> JSONResponse:
)
if template.get("deploy", False):
- manager.deploy_service_onchain(hash=service.hash, update=update)
- manager.stake_service_on_chain(hash=service.hash)
- manager.fund_service(hash=service.hash)
+ manager.deploy_service_onchain_from_safe(hash=service.hash, update=update)
+ manager.stake_service_on_chain_from_safe(hash=service.hash)
manager.deploy_service_locally(hash=service.hash)
- schedule_funding_job(service=service.hash)
return JSONResponse(
content=operate.service_manager().create_or_load(hash=service.hash).json
@@ -521,11 +532,9 @@ async def _update_services(request: Request) -> JSONResponse:
)
if template.get("deploy", False):
manager = operate.service_manager()
- manager.deploy_service_onchain(hash=service.hash, update=True)
- manager.stake_service_on_chain(hash=service.hash)
- manager.fund_service(hash=service.hash)
+ manager.deploy_service_onchain_from_safe(hash=service.hash, update=True)
+ manager.stake_service_on_chain_from_safe(hash=service.hash)
manager.deploy_service_locally(hash=service.hash)
- schedule_funding_job(service=service.hash)
return JSONResponse(content=service.json)
@app.get("/api/services/{service}")
diff --git a/operate/ledger/__init__.py b/operate/ledger/__init__.py
index b0352423d..078a93d1e 100644
--- a/operate/ledger/__init__.py
+++ b/operate/ledger/__init__.py
@@ -28,15 +28,13 @@
from operate.types import ChainType, LedgerType
-ETHEREUM_PUBLIC_RPC = "https://ethereum.publicnode.com"
-GNOSIS_PUBLIC_RPC = "https://gnosis-rpc.publicnode.com"
-GOERLI_PUBLIC_RPC = "https://ethereum-goerli.publicnode.com"
-SOLANA_PUBLIC_RPC = "https://api.mainnet-beta.solana.com"
+ETHEREUM_PUBLIC_RPC = os.environ.get("DEV_RPC", "https://ethereum.publicnode.com")
+GNOSIS_PUBLIC_RPC = os.environ.get("DEV_RPC", "https://gnosis-rpc.publicnode.com")
+GOERLI_PUBLIC_RPC = os.environ.get("DEV_RPC", "https://ethereum-goerli.publicnode.com")
+SOLANA_PUBLIC_RPC = os.environ.get("DEV_RPC", "https://api.mainnet-beta.solana.com")
ETHEREUM_RPC = os.environ.get("DEV_RPC", "https://ethereum.publicnode.com")
-GNOSIS_RPC = os.environ.get(
- "DEV_RPC", "https://go.getblock.io/2a1fa1ade5d547ca86eab099c35ce2a7"
-)
+GNOSIS_RPC = os.environ.get("DEV_RPC", "https://rpc-gate.autonolas.tech/gnosis-rpc/")
GOERLI_RPC = os.environ.get("DEV_RPC", "https://ethereum-goerli.publicnode.com")
SOLANA_RPC = os.environ.get("DEV_RPC", "https://api.mainnet-beta.solana.com")
diff --git a/operate/services/manage.py b/operate/services/manage.py
index b6bc540ba..182d9300c 100644
--- a/operate/services/manage.py
+++ b/operate/services/manage.py
@@ -33,7 +33,7 @@
from operate.keys import Key, KeysManager
from operate.ledger import PUBLIC_RPCS
from operate.ledger.profiles import CONTRACTS, OLAS, STAKING
-from operate.services.protocol import OnChainManager, StakingState
+from operate.services.protocol import EthSafeTxBuilder, OnChainManager, StakingState
from operate.services.service import (
Deployment,
OnChainData,
@@ -108,6 +108,14 @@ def get_on_chain_manager(self, service: Service) -> OnChainManager:
contracts=CONTRACTS[service.ledger_config.chain],
)
+ def get_eth_safe_tx_builder(self, service: Service) -> EthSafeTxBuilder:
+ """Get EthSafeTxBuilder instance."""
+ return EthSafeTxBuilder(
+ rpc=service.ledger_config.rpc,
+ wallet=self.wallet_manager.load(service.ledger_config.type),
+ contracts=CONTRACTS[service.ledger_config.chain],
+ )
+
def create_or_load(
self,
hash: str,
@@ -289,6 +297,233 @@ def deploy_service_onchain( # pylint: disable=too-many-statements
)
service.store()
+ def deploy_service_onchain_from_safe( # pylint: disable=too-many-statements,too-many-locals
+ self,
+ hash: str,
+ update: bool = False,
+ ) -> None:
+ """
+ Deploy as service on-chain
+
+ :param hash: Service hash
+ :param update: Update the existing deployment
+ """
+ self.logger.info("Loading service")
+ service = self.create_or_load(hash=hash)
+ user_params = service.chain_data.user_params
+ keys = service.keys or [
+ self.keys_manager.get(self.keys_manager.create())
+ for _ in range(service.helper.config.number_of_agents)
+ ]
+ instances = [key.address for key in keys]
+ wallet = self.wallet_manager.load(service.ledger_config.type)
+ sftxb = self.get_eth_safe_tx_builder(service=service)
+ if user_params.use_staking and not sftxb.staking_slots_available(
+ staking_contract=STAKING[service.ledger_config.chain]
+ ):
+ raise ValueError("No staking slots available")
+
+ if service.chain_data.token > -1:
+ self.logger.info("Syncing service state")
+ info = sftxb.info(token_id=service.chain_data.token)
+ service.chain_data.on_chain_state = OnChainState(info["service_state"])
+ service.chain_data.instances = info["instances"]
+ service.chain_data.multisig = info["multisig"]
+ service.store()
+ self.logger.info(f"Service state: {service.chain_data.on_chain_state.name}")
+
+ if user_params.use_staking:
+ self.logger.info("Checking staking compatibility")
+ if service.chain_data.on_chain_state in (
+ OnChainState.NOTMINTED,
+ OnChainState.MINTED,
+ ):
+ required_olas = (
+ user_params.olas_cost_of_bond + user_params.olas_required_to_stake
+ )
+ elif service.chain_data.on_chain_state == OnChainState.ACTIVATED:
+ required_olas = user_params.olas_required_to_stake
+ else:
+ required_olas = 0
+
+ balance = (
+ registry_contracts.erc20.get_instance(
+ ledger_api=sftxb.ledger_api,
+ contract_address=OLAS[service.ledger_config.chain],
+ )
+ .functions.balanceOf(wallet.safe)
+ .call()
+ )
+ if balance < required_olas:
+ raise ValueError(
+ "You don't have enough olas to stake, "
+ f"address: {wallet.safe}; required olas: {required_olas}; your balance {balance}"
+ )
+
+ if service.chain_data.on_chain_state == OnChainState.NOTMINTED:
+ self.logger.info("Minting service")
+ receipt = (
+ sftxb.new_tx()
+ .add(
+ sftxb.get_mint_tx_data(
+ package_path=service.service_path,
+ agent_id=user_params.agent_id,
+ number_of_slots=service.helper.config.number_of_agents,
+ cost_of_bond=(
+ user_params.olas_cost_of_bond
+ if user_params.use_staking
+ else user_params.cost_of_bond
+ ),
+ threshold=user_params.threshold,
+ nft=IPFSHash(user_params.nft),
+ update_token=service.chain_data.token if update else None,
+ token=(
+ OLAS[service.ledger_config.chain]
+ if user_params.use_staking
+ else None
+ ),
+ )
+ )
+ .settle()
+ )
+ event_data, *_ = t.cast(
+ t.Tuple,
+ registry_contracts.service_registry.process_receipt(
+ ledger_api=sftxb.ledger_api,
+ contract_address="0x9338b5153AE39BB89f50468E608eD9d764B755fD",
+ event="CreateService",
+ receipt=receipt,
+ ).get("events"),
+ )
+ service.chain_data.token = event_data["args"]["serviceId"]
+ service.chain_data.on_chain_state = OnChainState.MINTED
+ service.store()
+
+ if service.chain_data.on_chain_state == OnChainState.MINTED:
+ cost_of_bond = user_params.cost_of_bond
+ if user_params.use_staking:
+ token_utility = "0xa45E64d13A30a51b91ae0eb182e88a40e9b18eD8"
+ olas_token = "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f"
+ self.logger.info(
+ f"Approving OLAS as bonding token from {wallet.safe} to {token_utility}"
+ )
+ cost_of_bond = (
+ registry_contracts.service_registry_token_utility.get_agent_bond(
+ ledger_api=sftxb.ledger_api,
+ contract_address=token_utility,
+ service_id=service.chain_data.token,
+ agent_id=user_params.agent_id,
+ ).get("bond")
+ )
+ sftxb.new_tx().add(
+ sftxb.get_olas_approval_data(
+ spender=token_utility,
+ amount=cost_of_bond,
+ olas_contract=olas_token,
+ )
+ ).settle()
+ token_utility_allowance = (
+ registry_contracts.erc20.get_instance(
+ ledger_api=sftxb.ledger_api,
+ contract_address=olas_token,
+ )
+ .functions.allowance(
+ wallet.safe,
+ token_utility,
+ )
+ .call()
+ )
+ self.logger.info(
+ f"Approved {token_utility_allowance} OLAS from {wallet.safe} to {token_utility}"
+ )
+
+ self.logger.info("Activating service")
+ sftxb.new_tx().add(
+ sftxb.get_activate_data(
+ service_id=service.chain_data.token,
+ cost_of_bond=cost_of_bond,
+ )
+ ).settle()
+ service.chain_data.on_chain_state = OnChainState.ACTIVATED
+ service.store()
+
+ if service.chain_data.on_chain_state == OnChainState.ACTIVATED:
+ cost_of_bond = user_params.cost_of_bond
+ if user_params.use_staking:
+ token_utility = "0xa45E64d13A30a51b91ae0eb182e88a40e9b18eD8"
+ olas_token = "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f"
+ self.logger.info(
+ f"Approving OLAS as bonding token from {wallet.safe} to {token_utility}"
+ )
+ cost_of_bond = (
+ registry_contracts.service_registry_token_utility.get_agent_bond(
+ ledger_api=sftxb.ledger_api,
+ contract_address=token_utility,
+ service_id=service.chain_data.token,
+ agent_id=user_params.agent_id,
+ ).get("bond")
+ )
+ sftxb.new_tx().add(
+ sftxb.get_olas_approval_data(
+ spender=token_utility,
+ amount=cost_of_bond,
+ olas_contract=olas_token,
+ )
+ ).settle()
+ token_utility_allowance = (
+ registry_contracts.erc20.get_instance(
+ ledger_api=sftxb.ledger_api,
+ contract_address=olas_token,
+ )
+ .functions.allowance(
+ wallet.safe,
+ token_utility,
+ )
+ .call()
+ )
+ self.logger.info(
+ f"Approved {token_utility_allowance} OLAS from {wallet.safe} to {token_utility}"
+ )
+ cost_of_bond = 0
+
+ self.logger.info(
+ f"Registering service: {service.chain_data.token} -> {instances}"
+ )
+ sftxb.new_tx().add(
+ sftxb.get_register_instances_data(
+ service_id=service.chain_data.token,
+ instances=instances,
+ agents=[user_params.agent_id for _ in instances],
+ cost_of_bond=cost_of_bond,
+ )
+ ).settle()
+ service.chain_data.on_chain_state = OnChainState.REGISTERED
+ service.keys = keys
+ service.store()
+
+ if service.chain_data.on_chain_state == OnChainState.REGISTERED:
+ self.logger.info("Deploying service")
+ sftxb.new_tx().add(
+ sftxb.get_deploy_data(
+ service_id=service.chain_data.token,
+ reuse_multisig=update,
+ )
+ ).settle()
+ service.chain_data.on_chain_state = OnChainState.DEPLOYED
+ service.store()
+
+ info = sftxb.info(token_id=service.chain_data.token)
+ service.keys = keys
+ service.chain_data = OnChainData(
+ token=service.chain_data.token,
+ instances=info["instances"],
+ multisig=info["multisig"],
+ staked=False,
+ on_chain_state=service.chain_data.on_chain_state,
+ user_params=service.chain_data.user_params,
+ )
+ service.store()
+
def terminate_service_on_chain(self, hash: str) -> None:
"""
Terminate service on-chain
@@ -313,6 +548,27 @@ def terminate_service_on_chain(self, hash: str) -> None:
service.chain_data.on_chain_state = OnChainState.TERMINATED
service.store()
+ def terminate_service_on_chain_from_safe(self, hash: str) -> None:
+ """
+ Terminate service on-chain
+
+ :param hash: Service hash
+ """
+ service = self.create_or_load(hash=hash)
+ if service.chain_data.on_chain_state != OnChainState.DEPLOYED:
+ self.logger.info("Cannot terminate service")
+ return
+
+ self.logger.info("Terminating service")
+ sftxb = self.get_eth_safe_tx_builder(service=service)
+ sftxb.new_tx().add(
+ sftxb.get_terminate_data(
+ service_id=service.chain_data.token,
+ )
+ ).settle()
+ service.chain_data.on_chain_state = OnChainState.TERMINATED
+ service.store()
+
def unbond_service_on_chain(self, hash: str) -> None:
"""
Unbond service on-chain
@@ -337,6 +593,27 @@ def unbond_service_on_chain(self, hash: str) -> None:
service.chain_data.on_chain_state = OnChainState.UNBONDED
service.store()
+ def unbond_service_on_chain_from_safe(self, hash: str) -> None:
+ """
+ Terminate service on-chain
+
+ :param hash: Service hash
+ """
+ service = self.create_or_load(hash=hash)
+ if service.chain_data.on_chain_state != OnChainState.TERMINATED:
+ self.logger.info("Cannot unbond service")
+ return
+
+ self.logger.info("Unbonding service")
+ sftxb = self.get_eth_safe_tx_builder(service=service)
+ sftxb.new_tx().add(
+ sftxb.get_unbond_data(
+ service_id=service.chain_data.token,
+ )
+ ).settle()
+ service.chain_data.on_chain_state = OnChainState.TERMINATED
+ service.store()
+
def stake_service_on_chain(self, hash: str) -> None:
"""
Stake service on-chain
@@ -373,6 +650,54 @@ def stake_service_on_chain(self, hash: str) -> None:
service.chain_data.staked = True
service.store()
+ def stake_service_on_chain_from_safe(self, hash: str) -> None:
+ """
+ Stake service on-chain
+
+ :param hash: Service hash
+ """
+ service = self.create_or_load(hash=hash)
+ if not service.chain_data.user_params.use_staking:
+ self.logger.info("Cannot stake service, `use_staking` is set to false")
+ return
+
+ if service.chain_data.on_chain_state != OnChainState.DEPLOYED:
+ self.logger.info("Cannot stake service, it's not in deployed state")
+ return
+
+ sftxb = self.get_eth_safe_tx_builder(service=service)
+ state = sftxb.staking_status(
+ service_id=service.chain_data.token,
+ staking_contract=STAKING[service.ledger_config.chain],
+ )
+ self.logger.info(f"Checking staking status for: {service.chain_data.token}")
+ if state == StakingState.STAKED:
+ self.logger.info(f"{service.chain_data.token} is already staked")
+ service.chain_data.staked = True
+ service.store()
+ return
+
+ self.logger.info(f"Approving staking: {service.chain_data.token}")
+ sftxb.new_tx().add(
+ sftxb.get_staking_approval_data(
+ service_id=service.chain_data.token,
+ service_registry=CONTRACTS[service.ledger_config.chain][
+ "service_registry"
+ ],
+ staking_contract=STAKING[service.ledger_config.chain],
+ )
+ ).settle()
+
+ self.logger.info(f"Staking service: {service.chain_data.token}")
+ sftxb.new_tx().add(
+ sftxb.get_staking_data(
+ service_id=service.chain_data.token,
+ staking_contract=STAKING[service.ledger_config.chain],
+ )
+ ).settle()
+ service.chain_data.staked = True
+ service.store()
+
def unstake_service_on_chain(self, hash: str) -> None:
"""
Unbond service on-chain
@@ -395,6 +720,7 @@ def unstake_service_on_chain(self, hash: str) -> None:
service.store()
return
+ self.logger.info(f"Unstaking service: {service.chain_data.token}")
ocm.unstake(
service_id=service.chain_data.token,
staking_contract=STAKING[service.ledger_config.chain],
@@ -402,6 +728,44 @@ def unstake_service_on_chain(self, hash: str) -> None:
service.chain_data.staked = False
service.store()
+ def unstake_service_on_chain_from_safe(self, hash: str) -> None:
+ """
+ Unbond service on-chain
+
+ :param hash: Service hash
+ """
+ service = self.create_or_load(hash=hash)
+ if not service.chain_data.user_params.use_staking:
+ self.logger.info("Cannot unstake service, `use_staking` is set to false")
+ return
+
+ sftxb = self.get_eth_safe_tx_builder(service=service)
+ state = sftxb.staking_status(
+ service_id=service.chain_data.token,
+ staking_contract=STAKING[service.ledger_config.chain],
+ )
+ self.logger.info(f"Checking staking status for: {service.chain_data.token}")
+ if state != StakingState.STAKED:
+ self.logger.info("Cannot unstake service, it's not staked")
+ service.chain_data.staked = False
+ service.store()
+ return
+
+ self.logger.info(f"Unstaking service: {service.chain_data.token}")
+ sftxb.new_tx().add(
+ sftxb.get_unstaking_data(
+ service_id=service.chain_data.token,
+ staking_contract=STAKING[service.ledger_config.chain],
+ )
+ ).add(
+ sftxb.get_staking_data(
+ service_id=service.chain_data.token,
+ staking_contract=STAKING[service.ledger_config.chain],
+ )
+ ).settle()
+ service.chain_data.staked = True
+ service.store()
+
def fund_service(
self,
hash: str,
@@ -507,27 +871,51 @@ def update_service(
new_hash: str,
rpc: t.Optional[str] = None,
on_chain_user_params: t.Optional[OnChainUserParams] = None,
+ from_safe: bool = True,
) -> Service:
"""Update a service."""
old_service = self.create_or_load(
hash=old_hash,
)
- self.unstake_service_on_chain(
+ (
+ self.unstake_service_on_chain_from_safe
+ if from_safe
+ else self.unstake_service_on_chain
+ )(
hash=old_hash,
)
- self.terminate_service_on_chain(
+ (
+ self.terminate_service_on_chain_from_safe
+ if from_safe
+ else self.terminate_service_on_chain
+ )(
hash=old_hash,
)
- self.unbond_service_on_chain(
+ (
+ self.unbond_service_on_chain_from_safe
+ if from_safe
+ else self.unbond_service_on_chain
+ )(
hash=old_hash,
)
- ocm = self.get_on_chain_manager(service=old_service)
+
owner, *_ = old_service.chain_data.instances
- ocm.swap(
- service_id=old_service.chain_data.token,
- multisig=old_service.chain_data.multisig,
- owner_key=str(self.keys_manager.get(key=owner).private_key),
- )
+ if from_safe:
+ sftx = self.get_eth_safe_tx_builder(service=old_service)
+ sftx.new_tx().add(
+ sftx.get_swap_data(
+ service_id=old_service.chain_data.token,
+ multisig=old_service.chain_data.multisig,
+ owner_key=str(self.keys_manager.get(key=owner).private_key),
+ )
+ ).settle()
+ else:
+ ocm = self.get_on_chain_manager(service=old_service)
+ ocm.swap(
+ service_id=old_service.chain_data.token,
+ multisig=old_service.chain_data.multisig,
+ owner_key=str(self.keys_manager.get(key=owner).private_key),
+ )
new_service = self.create_or_load(
hash=new_hash,
diff --git a/operate/services/protocol.py b/operate/services/protocol.py
index 1c665fbf2..07ccdf4e5 100644
--- a/operate/services/protocol.py
+++ b/operate/services/protocol.py
@@ -27,6 +27,7 @@
import tempfile
import time
import typing as t
+from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Optional, Union
@@ -37,7 +38,16 @@
from aea_ledger_ethereum.ethereum import EthereumCrypto
from autonomy.chain.base import registry_contracts
from autonomy.chain.config import ChainConfigs, ChainType, ContractConfigs
-from autonomy.chain.service import get_agent_instances, get_service_info
+from autonomy.chain.constants import (
+ GNOSIS_SAFE_PROXY_FACTORY_CONTRACT,
+ GNOSIS_SAFE_SAME_ADDRESS_MULTISIG_CONTRACT,
+)
+from autonomy.chain.service import (
+ get_agent_instances,
+ get_delployment_payload,
+ get_reuse_multisig_payload,
+ get_service_info,
+)
from autonomy.chain.tx import TxSettler
from autonomy.cli.helpers.chain import MintHelper as MintManager
from autonomy.cli.helpers.chain import OnChainHelper
@@ -63,6 +73,9 @@
from operate.wallet.master import MasterWallet
+ETHEREUM_ERC20 = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
+
+
class StakingState(Enum):
"""Staking state enumeration for the staking."""
@@ -71,6 +84,105 @@ class StakingState(Enum):
EVICTED = 2
+class GnosisSafeTransaction:
+ """Safe transaction"""
+
+ def __init__(
+ self,
+ ledger_api: LedgerApi,
+ crypto: Crypto,
+ chain_type: ChainType,
+ safe: str,
+ ) -> None:
+ """Initiliaze a Gnosis safe tx"""
+ self.ledger_api = ledger_api
+ self.crypto = crypto
+ self.chain_type = chain_type
+ self.safe = safe
+ self._txs: t.List[t.Dict] = []
+ self.tx: t.Optional[t.Dict] = None
+
+ def add(self, tx: t.Dict) -> "GnosisSafeTransaction":
+ """Add a transaction"""
+ self._txs.append(tx)
+ return self
+
+ def build(self) -> t.Dict:
+ """Build the transaction."""
+ multisend_data = bytes.fromhex(
+ registry_contracts.multisend.get_tx_data(
+ ledger_api=self.ledger_api,
+ contract_address=ContractConfigs.multisend.contracts[self.chain_type],
+ multi_send_txs=self._txs,
+ ).get("data")[2:]
+ )
+ safe_tx_hash = registry_contracts.gnosis_safe.get_raw_safe_transaction_hash(
+ ledger_api=self.ledger_api,
+ contract_address=self.safe,
+ value=0,
+ safe_tx_gas=0,
+ to_address=ContractConfigs.multisend.contracts[self.chain_type],
+ data=multisend_data,
+ operation=SafeOperation.DELEGATE_CALL.value,
+ ).get("tx_hash")[2:]
+ payload_data = hash_payload_to_hex(
+ safe_tx_hash=safe_tx_hash,
+ ether_value=0,
+ safe_tx_gas=0,
+ to_address=ContractConfigs.multisend.contracts[self.chain_type],
+ operation=SafeOperation.DELEGATE_CALL.value,
+ data=multisend_data,
+ )
+ owner = self.ledger_api.api.to_checksum_address(self.crypto.address)
+ tx_params = skill_input_hex_to_payload(payload=payload_data)
+ safe_tx_bytes = binascii.unhexlify(tx_params["safe_tx_hash"])
+ signatures = {
+ owner: self.crypto.sign_message(
+ message=safe_tx_bytes,
+ is_deprecated_mode=True,
+ )[2:]
+ }
+ tx = registry_contracts.gnosis_safe.get_raw_safe_transaction(
+ ledger_api=self.ledger_api,
+ contract_address=self.safe,
+ sender_address=owner,
+ owners=(owner,), # type: ignore
+ to_address=tx_params["to_address"],
+ value=tx_params["ether_value"],
+ data=tx_params["data"],
+ safe_tx_gas=tx_params["safe_tx_gas"],
+ signatures_by_owner=signatures,
+ operation=SafeOperation.DELEGATE_CALL.value,
+ nonce=self.ledger_api.api.eth.get_transaction_count(owner),
+ )
+ self.tx = self.crypto.sign_transaction(tx)
+ return t.cast(t.Dict, self.tx)
+
+ def settle(self) -> t.Dict:
+ """Settle the transaction."""
+ retries = 0
+ deadline = datetime.now().timestamp() + ON_CHAIN_INTERACT_TIMEOUT
+ while (
+ retries < ON_CHAIN_INTERACT_RETRIES
+ and datetime.now().timestamp() < deadline
+ ):
+ try:
+ self.build()
+ tx_digest = self.ledger_api.send_signed_transaction(self.tx)
+ except Exception as e: # pylint: disable=broad-except
+ print(f"Error sending the safe tx: {e}")
+ tx_digest = None
+
+ if tx_digest is not None:
+ receipt = self.ledger_api.api.eth.wait_for_transaction_receipt(
+ tx_digest
+ )
+ if receipt["status"] != 0:
+ return receipt
+ time.sleep(ON_CHAIN_INTERACT_SLEEP)
+ raise RuntimeError("Timeout while waiting for safe transaction to go through")
+
+
class StakingManager(OnChainHelper):
"""Helper class for staking a service."""
@@ -128,13 +240,12 @@ def service_info(self, staking_contract: str, service_id: int) -> dict:
service_id,
).get("data")
- def stake(
+ def check_staking_compatibility(
self,
service_id: int,
- service_registry: str,
staking_contract: str,
) -> None:
- """Stake the service"""
+ """Check if service can be staked."""
status = self.status(service_id, staking_contract)
if status == StakingState.STAKED:
raise ValueError("Service already stacked")
@@ -145,6 +256,17 @@ def stake(
if not self.slots_available(staking_contract):
raise ValueError("No sataking slots available.")
+ def stake(
+ self,
+ service_id: int,
+ service_registry: str,
+ staking_contract: str,
+ ) -> None:
+ """Stake the service"""
+ self.check_staking_compatibility(
+ service_id=service_id, staking_contract=staking_contract
+ )
+
tx_settler = TxSettler(
ledger_api=self.ledger_api,
crypto=self.crypto,
@@ -154,34 +276,28 @@ def stake(
sleep=ON_CHAIN_INTERACT_SLEEP,
)
- # Check if token usage is already approved
- if service_id not in self.staking_ctr.get_service_ids(
- ledger_api=self.ledger_api,
- contract_address=staking_contract,
- ):
- # we make use of the ERC20 contract to build the approval transaction
- # since it has the same interface as ERC721 we might want to create
- # a ERC721 contract package
-
- def _build_approval_tx( # pylint: disable=unused-argument
- *args: t.Any, **kargs: t.Any
- ) -> t.Dict:
- return registry_contracts.erc20.get_approve_tx(
- ledger_api=self.ledger_api,
- contract_address=service_registry,
- spender=staking_contract,
- sender=self.crypto.address,
- amount=service_id,
- )
-
- setattr(tx_settler, "build", _build_approval_tx) # noqa: B010
- tx_settler.transact(
- method=lambda: {},
- contract="",
- kwargs={},
- dry_run=False,
+ # we make use of the ERC20 contract to build the approval transaction
+ # since it has the same interface as ERC721 we might want to create
+ # a ERC721 contract package
+ def _build_approval_tx( # pylint: disable=unused-argument
+ *args: t.Any, **kargs: t.Any
+ ) -> t.Dict:
+ return registry_contracts.erc20.get_approve_tx(
+ ledger_api=self.ledger_api,
+ contract_address=service_registry,
+ spender=staking_contract,
+ sender=self.crypto.address,
+ amount=service_id,
)
+ setattr(tx_settler, "build", _build_approval_tx) # noqa: B010
+ tx_settler.transact(
+ method=lambda: {},
+ contract="",
+ kwargs={},
+ dry_run=False,
+ )
+
def _build_staking_tx( # pylint: disable=unused-argument
*args: t.Any, **kargs: t.Any
) -> t.Dict:
@@ -206,12 +322,18 @@ def _build_staking_tx( # pylint: disable=unused-argument
dry_run=False,
)
- def _can_unstake_service(
+ def check_if_unstaking_possible(
self,
service_id: int,
staking_contract: str,
- ) -> bool:
+ ) -> None:
"""Check unstaking availability"""
+ if (
+ self.status(service_id=service_id, staking_contract=staking_contract)
+ != StakingState.STAKED
+ ):
+ raise ValueError("Service not staked.")
+
ts_start = t.cast(int, self.service_info(staking_contract, service_id)[3])
available_rewards = t.cast(
int,
@@ -227,19 +349,10 @@ def _can_unstake_service(
)
staked_duration = time.time() - ts_start
if staked_duration < minimum_staking_duration and available_rewards > 0:
- return False
- return True
+ raise ValueError("Service cannot be unstaked yet.")
def unstake(self, service_id: int, staking_contract: str) -> None:
"""Unstake the service"""
- if (
- self.status(service_id=service_id, staking_contract=staking_contract)
- != StakingState.STAKED
- ):
- raise ValueError("Service not staked.")
-
- if not self._can_unstake_service(service_id, staking_contract):
- raise ValueError("Service cannot be unstaked yet.")
tx_settler = TxSettler(
ledger_api=self.ledger_api,
@@ -274,8 +387,58 @@ def _build_unstaking_tx( # pylint: disable=unused-argument
dry_run=False,
)
+ def get_stake_approval_tx_data(
+ self,
+ service_id: int,
+ service_registry: str,
+ staking_contract: str,
+ ) -> bytes:
+ """Get stake approval tx data."""
+ self.check_staking_compatibility(
+ service_id=service_id,
+ staking_contract=staking_contract,
+ )
+ return registry_contracts.erc20.get_instance(
+ ledger_api=self.ledger_api,
+ contract_address=service_registry,
+ ).encodeABI(
+ fn_name="approve",
+ args=[
+ staking_contract,
+ service_id,
+ ],
+ )
+
+ def get_stake_tx_data(self, service_id: int, staking_contract: str) -> bytes:
+ """Get stake approval tx data."""
+ self.check_staking_compatibility(
+ service_id=service_id,
+ staking_contract=staking_contract,
+ )
+ return self.staking_ctr.get_instance(
+ ledger_api=self.ledger_api,
+ contract_address=staking_contract,
+ ).encodeABI(
+ fn_name="stake",
+ args=[service_id],
+ )
+
+ def get_unstake_tx_data(self, service_id: int, staking_contract: str) -> bytes:
+ """Unstake the service"""
+ self.check_if_unstaking_possible(
+ service_id=service_id,
+ staking_contract=staking_contract,
+ )
+ return self.staking_ctr.get_instance(
+ ledger_api=self.ledger_api,
+ contract_address=staking_contract,
+ ).encodeABI(
+ fn_name="unstake",
+ args=[service_id],
+ )
-class OnChainManager:
+
+class _ChainUtil:
"""On chain service management."""
def __init__(
@@ -359,6 +522,10 @@ def info(self, token_id: int) -> t.Dict:
instances=instances,
)
+
+class OnChainManager(_ChainUtil):
+ """On chain service management."""
+
def mint( # pylint: disable=too-many-arguments,too-many-locals
self,
package_path: Path,
@@ -685,3 +852,316 @@ def staking_status(self, service_id: int, staking_contract: str) -> StakingState
service_id=service_id,
staking_contract=staking_contract,
)
+
+
+class EthSafeTxBuilder(_ChainUtil):
+ """Safe Transaction builder."""
+
+ def new_tx(self) -> GnosisSafeTransaction:
+ """Create a new GnosisSafeTransaction instance."""
+ return GnosisSafeTransaction(
+ ledger_api=self.ledger_api,
+ crypto=self.crypto,
+ chain_type=self.chain_type,
+ safe=t.cast(str, self.wallet.safe),
+ )
+
+ def get_mint_tx_data( # pylint: disable=too-many-arguments
+ self,
+ package_path: Path,
+ agent_id: int,
+ number_of_slots: int,
+ cost_of_bond: int,
+ threshold: int,
+ nft: Optional[Union[Path, IPFSHash]],
+ update_token: t.Optional[int] = None,
+ token: t.Optional[str] = None,
+ ) -> t.Dict:
+ """Build mint transaction."""
+ # TODO: Support for update
+ self._patch()
+ manager = MintManager(
+ chain_type=self.chain_type,
+ key=self.wallet.key_path,
+ password=self.wallet.password,
+ update_token=update_token,
+ timeout=ON_CHAIN_INTERACT_TIMEOUT,
+ retries=ON_CHAIN_INTERACT_RETRIES,
+ sleep=ON_CHAIN_INTERACT_SLEEP,
+ )
+ # Prepare for minting
+ (
+ manager.load_package_configuration(
+ package_path=package_path, package_type=PackageType.SERVICE
+ )
+ .load_metadata()
+ .verify_nft(nft=nft)
+ .verify_service_dependencies(agent_id=agent_id)
+ .publish_metadata()
+ )
+ instance = registry_contracts.service_manager.get_instance(
+ ledger_api=self.ledger_api,
+ contract_address=self.contracts["service_manager"],
+ )
+
+ txd = instance.encodeABI(
+ fn_name="create" if update_token is None else "update",
+ args=[
+ self.wallet.safe,
+ token or ETHEREUM_ERC20,
+ manager.metadata_hash,
+ [agent_id],
+ [[number_of_slots, cost_of_bond]],
+ threshold,
+ ],
+ )
+
+ return {
+ "to": self.contracts["service_manager"],
+ "data": txd[2:],
+ "operation": MultiSendOperation.CALL,
+ "value": 0,
+ }
+
+ def get_olas_approval_data(
+ self,
+ spender: str,
+ amount: int,
+ olas_contract: str,
+ ) -> t.Dict:
+ """Get activate tx data."""
+ instance = registry_contracts.erc20.get_instance(
+ ledger_api=self.ledger_api,
+ contract_address=olas_contract,
+ )
+ txd = instance.encodeABI(
+ fn_name="approve",
+ args=[spender, amount],
+ )
+ return {
+ "to": olas_contract,
+ "data": txd[2:],
+ "operation": MultiSendOperation.CALL,
+ "value": 0,
+ }
+
+ def get_activate_data(self, service_id: int, cost_of_bond: int) -> t.Dict:
+ """Get activate tx data."""
+ instance = registry_contracts.service_manager.get_instance(
+ ledger_api=self.ledger_api,
+ contract_address=self.contracts["service_manager"],
+ )
+ txd = instance.encodeABI(
+ fn_name="activateRegistration",
+ args=[service_id],
+ )
+ return {
+ "from": self.wallet.safe,
+ "to": self.contracts["service_manager"],
+ "data": txd[2:],
+ "operation": MultiSendOperation.CALL,
+ "value": cost_of_bond,
+ }
+
+ def get_register_instances_data(
+ self,
+ service_id: int,
+ instances: t.List[str],
+ agents: t.List[int],
+ cost_of_bond: int,
+ ) -> t.Dict:
+ """Get register instances tx data."""
+ instance = registry_contracts.service_manager.get_instance(
+ ledger_api=self.ledger_api,
+ contract_address=self.contracts["service_manager"],
+ )
+ txd = instance.encodeABI(
+ fn_name="registerAgents",
+ args=[
+ service_id,
+ instances,
+ agents,
+ ],
+ )
+ return {
+ "from": self.wallet.safe,
+ "to": self.contracts["service_manager"],
+ "data": txd[2:],
+ "operation": MultiSendOperation.CALL,
+ "value": cost_of_bond,
+ }
+
+ def get_deploy_data(
+ self,
+ service_id: int,
+ reuse_multisig: bool = False,
+ ) -> t.Dict:
+ """Get deploy tx data."""
+ instance = registry_contracts.service_manager.get_instance(
+ ledger_api=self.ledger_api,
+ contract_address=self.contracts["service_manager"],
+ )
+ if reuse_multisig:
+ _deployment_payload, error = get_reuse_multisig_payload(
+ ledger_api=self.ledger_api,
+ crypto=self.crypto,
+ chain_type=self.chain_type,
+ service_id=service_id,
+ )
+ if _deployment_payload is None:
+ raise ValueError(error)
+ deployment_payload = _deployment_payload
+ gnosis_safe_multisig = ContractConfigs.get(
+ GNOSIS_SAFE_SAME_ADDRESS_MULTISIG_CONTRACT.name
+ ).contracts[self.chain_type]
+ else:
+ deployment_payload = get_delployment_payload()
+ gnosis_safe_multisig = ContractConfigs.get(
+ GNOSIS_SAFE_PROXY_FACTORY_CONTRACT.name
+ ).contracts[self.chain_type]
+
+ txd = instance.encodeABI(
+ fn_name="deploy",
+ args=[
+ service_id,
+ gnosis_safe_multisig,
+ deployment_payload,
+ ],
+ )
+ return {
+ "to": self.contracts["service_manager"],
+ "data": txd[2:],
+ "operation": MultiSendOperation.CALL,
+ "value": 0,
+ }
+
+ def get_terminate_data(self, service_id: int) -> t.Dict:
+ """Get terminate tx data."""
+ instance = registry_contracts.service_manager.get_instance(
+ ledger_api=self.ledger_api,
+ contract_address=self.contracts["service_manager"],
+ )
+ txd = instance.encodeABI(
+ fn_name="terminate",
+ args=[service_id],
+ )
+ return {
+ "to": self.contracts["service_manager"],
+ "data": txd[2:],
+ "operation": MultiSendOperation.CALL,
+ "value": 0,
+ }
+
+ def get_unbond_data(self, service_id: int) -> t.Dict:
+ """Get unbond tx data."""
+ instance = registry_contracts.service_manager.get_instance(
+ ledger_api=self.ledger_api,
+ contract_address=self.contracts["service_manager"],
+ )
+ txd = instance.encodeABI(
+ fn_name="unbond",
+ args=[service_id],
+ )
+ return {
+ "to": self.contracts["service_manager"],
+ "data": txd[2:],
+ "operation": MultiSendOperation.CALL,
+ "value": 0,
+ }
+
+ def get_staking_approval_data(
+ self,
+ service_id: int,
+ service_registry: str,
+ staking_contract: str,
+ ) -> t.Dict:
+ """Get staking approval data"""
+ self._patch()
+ txd = StakingManager(
+ key=self.wallet.key_path,
+ password=self.wallet.password,
+ chain_type=self.chain_type,
+ ).get_stake_approval_tx_data(
+ service_id=service_id,
+ service_registry=service_registry,
+ staking_contract=staking_contract,
+ )
+ return {
+ "from": self.wallet.safe,
+ "to": self.contracts["service_registry"],
+ "data": txd[2:],
+ "operation": MultiSendOperation.CALL,
+ "value": 0,
+ }
+
+ def get_staking_data(
+ self,
+ service_id: int,
+ staking_contract: str,
+ ) -> t.Dict:
+ """Get staking tx data"""
+ self._patch()
+ txd = StakingManager(
+ key=self.wallet.key_path,
+ password=self.wallet.password,
+ chain_type=self.chain_type,
+ ).get_stake_tx_data(
+ service_id=service_id,
+ staking_contract=staking_contract,
+ )
+ return {
+ "to": staking_contract,
+ "data": txd[2:],
+ "operation": MultiSendOperation.CALL,
+ "value": 0,
+ }
+
+ def get_unstaking_data(
+ self,
+ service_id: int,
+ staking_contract: str,
+ ) -> t.Dict:
+ """Get unstaking tx data"""
+ self._patch()
+ txd = StakingManager(
+ key=self.wallet.key_path,
+ password=self.wallet.password,
+ chain_type=self.chain_type,
+ ).get_unstake_tx_data(
+ service_id=service_id,
+ staking_contract=staking_contract,
+ )
+ return {
+ "to": staking_contract,
+ "data": txd[2:],
+ "operation": MultiSendOperation.CALL,
+ "value": 0,
+ }
+
+ def staking_slots_available(self, staking_contract: str) -> bool:
+ """Stake service."""
+ self._patch()
+ return StakingManager(
+ key=self.wallet.key_path,
+ password=self.wallet.password,
+ chain_type=self.chain_type,
+ ).slots_available(
+ staking_contract=staking_contract,
+ )
+
+ def staking_status(self, service_id: int, staking_contract: str) -> StakingState:
+ """Stake the service"""
+ self._patch()
+ return StakingManager(
+ key=self.wallet.key_path,
+ password=self.wallet.password,
+ chain_type=self.chain_type,
+ ).status(
+ service_id=service_id,
+ staking_contract=staking_contract,
+ )
+
+ def get_swap_data(self, service_id: int, multisig: str, owner_key: str) -> t.Dict:
+ """Swap safe owner."""
+ # TODO: Discuss implementation
+ raise NotImplementedError()
diff --git a/package.json b/package.json
index 9be8970b7..0c1072de4 100644
--- a/package.json
+++ b/package.json
@@ -52,5 +52,5 @@
"start": "electron .",
"build": "rm -rf dist/ && electron-builder build"
},
- "version": "0.1.0-rc15.9.7.9.2"
+ "version": "0.1.0-rc15.9.7.9.3"
}
diff --git a/scripts/fund.py b/scripts/fund.py
index a4724b508..324649833 100644
--- a/scripts/fund.py
+++ b/scripts/fund.py
@@ -11,13 +11,15 @@
load_dotenv()
+RPC = os.environ.get("DEV_RPC", "http://localhost:8545")
+
OLAS_CONTRACT_ADDRESS_GNOSIS = "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f"
def fund(address: str, amount: float = 10.0) -> None:
"""Fund an address."""
staking_keys_path = os.environ.get("STAKING_TEST_KEYS_PATH", None)
- ledger_api = EthereumApi(address="http://localhost:8545")
+ ledger_api = EthereumApi(address=RPC)
crypto = EthereumCrypto("scripts/keys/gnosis.txt")
tx = ledger_api.get_transfer_transaction(
sender_address=crypto.address,
diff --git a/scripts/setup_wallet.py b/scripts/setup_wallet.py
index 85728496b..f17dbdcb6 100644
--- a/scripts/setup_wallet.py
+++ b/scripts/setup_wallet.py
@@ -55,7 +55,7 @@
print(wallet)
print("Funding wallet: ", end="")
-fund(wallet["wallet"]["address"])
+fund(wallet["wallet"]["address"], amount=20)
print(
requests.post(
diff --git a/scripts/transfer_olas.py b/scripts/transfer_olas.py
new file mode 100644
index 000000000..a9768a783
--- /dev/null
+++ b/scripts/transfer_olas.py
@@ -0,0 +1,84 @@
+"""
+Script for transferring OLAS
+
+Usage:
+ python transfer_olas.py PATH_TO_KEY_CONTAINING_OLAS ADDRESS_TO_TRANSFER AMOUNT
+
+Example:
+ python transfer_olas.py keys/gnosis.txt 0xce11e14225575945b8e6dc0d4f2dd4c570f79d9f 2
+"""
+
+import json
+import os
+import sys
+from pathlib import Path
+
+from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto
+
+
+RPC = os.environ.get("DEV_RPC", "http://localhost:8545")
+OLAS_CONTRACT_ADDRESS_GNOSIS = "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f"
+WEI_MULTIPLIER = 1e18
+
+
+def fund(wallet: str, address: str, amount: float = 20.0) -> None:
+ """Fund wallet with OLAS token"""
+ staking_wallet = EthereumCrypto(wallet)
+ ledger_api = EthereumApi(address=RPC)
+ olas_contract = ledger_api.api.eth.contract(
+ address=ledger_api.api.to_checksum_address(OLAS_CONTRACT_ADDRESS_GNOSIS),
+ abi=json.loads(
+ Path(
+ "operate",
+ "data",
+ "contracts",
+ "uniswap_v2_erc20",
+ "build",
+ "IUniswapV2ERC20.json",
+ ).read_text(encoding="utf-8")
+ ).get("abi"),
+ )
+ print(
+ f"Balance of {address} = {olas_contract.functions.balanceOf(address).call()/1e18} OLAS"
+ )
+ print(f"Transferring {amount} OLAS from {staking_wallet.address} to {address}")
+
+ tx = olas_contract.functions.transfer(
+ address, int(amount * WEI_MULTIPLIER)
+ ).build_transaction(
+ {
+ "chainId": 100,
+ "gas": 100000,
+ "gasPrice": ledger_api.api.to_wei("50", "gwei"),
+ "nonce": ledger_api.api.eth.get_transaction_count(staking_wallet.address),
+ }
+ )
+
+ signed_txn = ledger_api.api.eth.account.sign_transaction(
+ tx, staking_wallet.private_key
+ )
+ ledger_api.api.eth.send_raw_transaction(signed_txn.rawTransaction)
+ print(
+ f"Balance of {address} = {olas_contract.functions.balanceOf(address).call()/1e18} OLAS"
+ )
+
+
+if __name__ == "__main__":
+ args = sys.argv[1:]
+ if len(args) == 2:
+ fund(wallet=args[0], address=args[1])
+ sys.exit()
+
+ if len(args) == 3:
+ fund(wallet=args[0], address=args[1], amount=float(args[2]))
+ sys.exit()
+
+ print(
+ """Script for transferring OLAS
+
+Usage:
+ python transfer_olas.py PATH_TO_KEY_CONTAINING_OLAS ADDRESS_TO_TRANSFER AMOUNT
+
+Example:
+ python transfer_olas.py keys/gnosis.txt 0xce11e14225575945b8e6dc0d4f2dd4c570f79d9f 2"""
+ )