Skip to content

Commit

Permalink
Developer UI: Send transaction from an app (#133)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmv08 authored Apr 28, 2021
1 parent cc724a8 commit f93eae4
Show file tree
Hide file tree
Showing 39 changed files with 1,556 additions and 450 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ dist
.vscode
lerna-debug.log
yalc.lock
build
build
packages/safe-apps-developer-ui/src/types/contracts/
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@
"lerna": "^4.0.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"eslint": "7.23.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-prettier": "^3.3.1",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"eslint": "7.25.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"jest": "26.6.0",
"ts-jest": "^26.5.3",
"typescript": "^4.2.3"
"typescript": "^4.2.4"
}
}
35 changes: 21 additions & 14 deletions packages/safe-apps-developer-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,36 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@gnosis.pm/safe-apps-react-sdk": "2.3.0",
"@gnosis.pm/safe-apps-sdk": "^2.3.0",
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
"@material-ui/core": "^4.11.3",
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^13.0.16",
"@types/styled-components": "^5.1.9",
"@walletconnect/web3-provider": "^1.4.0",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^13.1.5",
"@walletconnect/web3-provider": "^1.4.1",
"clsx": "^1.1.1",
"ethereum-blockies-base64": "^1.0.2",
"ethers": "^5.0.32",
"ethers": "^5.1.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"styled-components": "^5.2.1",
"react-spring": "^9.1.2",
"styled-components": "^5.2.3",
"web3modal": "1.9.3",
"zustand": "^3.3.3"
"zustand": "^3.4.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --passWithNoTests",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"generate-types": "yarn generate-types:safe-contracts",
"generate-types:safe-contracts": "typechain --target=ethers-v5 --outDir './src/types/contracts' '../../node_modules/@gnosis.pm/safe-contracts/build/contracts/*.json'",
"postinstall": "yarn generate-types"
},
"browserslist": {
"production": [
Expand All @@ -43,18 +47,21 @@
]
},
"devDependencies": {
"@types/jest": "^26.0.22",
"@types/node": "^14.14.37",
"@types/react": "^17.0.3",
"@typechain/ethers-v5": "^6.0.5",
"@types/jest": "^26.0.23",
"@types/node": "^15.0.1",
"@types/react": "^17.0.4",
"@types/react-dom": "^17.0.3",
"@types/react-router": "^5.1.13",
"@types/react-router-dom": "^5.1.7",
"@types/styled-components": "^5.1.9",
"eslint-config-react-app": "^6.0.0",
"eslint-plugin-flowtype": "^5.4.0",
"eslint-plugin-flowtype": "^5.7.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-react": "^7.23.1",
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.2.0",
"typechain": "^4.0.3",
"web-vitals": "^1.1.1"
}
}
9 changes: 9 additions & 0 deletions packages/safe-apps-developer-ui/src/api/eth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ethers } from 'ethers';

const getEthBalance = async (provider: ethers.providers.BaseProvider, address: string): Promise<ethers.BigNumber> => {
const balance = provider.getBalance(address);

return balance;
};

export { getEthBalance };
11 changes: 11 additions & 0 deletions packages/safe-apps-developer-ui/src/api/safe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ethers } from 'ethers';
import { getSafeContract } from './safeContracts';

const getSafeNonce = async (signer: ethers.providers.JsonRpcSigner, safeAddress: string): Promise<ethers.BigNumber> => {
const safe = getSafeContract(safeAddress, signer);
const nonce = await safe.nonce();

return nonce;
};

export { getSafeNonce };
42 changes: 31 additions & 11 deletions packages/safe-apps-developer-ui/src/api/safeContracts.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,54 @@
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json';
import MultiSendSol from '@gnosis.pm/safe-contracts/build/contracts/MultiSend.json';
import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxyFactory.json';
import FallbackHandlerSol from '@gnosis.pm/safe-contracts/build/contracts/FallbackManager.json';
import { JsonRpcSigner } from '@ethersproject/providers';
import { Contract, ContractFactory } from 'ethers';
import { GnosisSafe, GnosisSafeProxyFactory, MultiSend, MasterCopy, FallbackManager } from 'src/types/contracts';

const getProxyFactoryContract = (address: string, signer: JsonRpcSigner): Contract =>
new Contract(address, ProxyFactorySol.abi, signer);
const getProxyFactoryContract = (address: string, signer: JsonRpcSigner): GnosisSafeProxyFactory =>
new Contract(address, ProxyFactorySol.abi, signer) as GnosisSafeProxyFactory;

const getSafeContract = (address: string, signer: JsonRpcSigner): Contract =>
new Contract(address, GnosisSafeSol.abi, signer);
const getSafeContract = (address: string, signer: JsonRpcSigner): GnosisSafe =>
new Contract(address, GnosisSafeSol.abi, signer) as GnosisSafe;

const deployMasterCopy = async (signer: JsonRpcSigner): Promise<Contract> => {
const getMultiSendContract = (address: string, signer: JsonRpcSigner): MultiSend =>
new Contract(address, MultiSendSol.abi, signer) as MultiSend;

const deployMasterCopy = async (signer: JsonRpcSigner): Promise<MasterCopy> => {
const factory = new ContractFactory(GnosisSafeSol.abi, GnosisSafeSol.bytecode, signer);
const masterCopy = await factory.deploy();

return masterCopy;
return masterCopy as MasterCopy;
};

const deployProxyFactory = async (signer: JsonRpcSigner): Promise<Contract> => {
const deployProxyFactory = async (signer: JsonRpcSigner): Promise<GnosisSafeProxyFactory> => {
const factory = new ContractFactory(ProxyFactorySol.abi, ProxyFactorySol.bytecode, signer);
const proxyFactory = await factory.deploy();

return proxyFactory;
return proxyFactory as GnosisSafeProxyFactory;
};

const deployFallbackHandler = async (signer: JsonRpcSigner): Promise<Contract> => {
const deployFallbackHandler = async (signer: JsonRpcSigner): Promise<FallbackManager> => {
const factory = new ContractFactory(FallbackHandlerSol.abi, FallbackHandlerSol.bytecode, signer);
const fallbackHandler = await factory.deploy();

return fallbackHandler;
return fallbackHandler as FallbackManager;
};

export { deployProxyFactory, deployFallbackHandler, deployMasterCopy, getSafeContract, getProxyFactoryContract };
const deployMultiSend = async (signer: JsonRpcSigner): Promise<MultiSend> => {
const factory = new ContractFactory(MultiSendSol.abi, MultiSendSol.bytecode, signer);
const multiSend = await factory.deploy();

return multiSend as MultiSend;
};

export {
deployProxyFactory,
deployFallbackHandler,
deployMasterCopy,
getSafeContract,
getProxyFactoryContract,
getMultiSendContract,
deployMultiSend,
};
10 changes: 10 additions & 0 deletions packages/safe-apps-developer-ui/src/api/signatures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { EMPTY_DATA } from 'src/utils/strings';

const getPreValidatedSignature = (from: string, startStr: string = EMPTY_DATA): string => {
return `${startStr}000000000000000000000000${from.replace(
EMPTY_DATA,
'',
)}000000000000000000000000000000000000000000000000000000000000000001`;
};

export { getPreValidatedSignature };
131 changes: 131 additions & 0 deletions packages/safe-apps-developer-ui/src/api/transactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { ZERO_ADDRESS } from 'src/utils/strings';
import { SignedProposedTx } from './../types/transaction';
import { Transaction } from '@gnosis.pm/safe-apps-sdk';
import { ethers } from 'ethers';
import { getSafeContract, getMultiSendContract } from 'src/api/safeContracts';
import { ProposedTx, CreateTransactionArgs } from 'src/types/transaction';
import { getPreValidatedSignature } from './signatures';
import { getSafeNonce } from './safe';

const CALL = 0;
const DELEGATE_CALL = 1;

const executeTransaction = async (
signer: ethers.providers.JsonRpcSigner,
safeAddress: string,
{ baseGas, data, gasPrice, gasToken, operation, refundReceiver, safeTxGas, to, valueInWei, sigs }: SignedProposedTx,
): Promise<ethers.ContractTransaction> => {
const safeInstance = getSafeContract(safeAddress, signer);
const execution = await safeInstance.execTransaction(
to,
valueInWei,
data,
operation,
safeTxGas,
baseGas,
gasPrice,
gasToken,
refundReceiver,
sigs,
);

return execution;
};

const getTransactionHash = async (
signer: ethers.providers.JsonRpcSigner,
safeAddress: string,
{
baseGas,
data,
gasPrice,
gasToken,
nonce,
operation,
refundReceiver,
safeTxGas,
to,
valueInWei,
}: ProposedTx & { nonce: ethers.BigNumberish },
): Promise<string> => {
const safeInstance = getSafeContract(safeAddress, signer);

const txHash = await safeInstance.getTransactionHash(
to,
valueInWei,
data,
operation,
safeTxGas,
baseGas,
gasPrice,
gasToken,
refundReceiver,
nonce,
);

return txHash;
};

const createTransaction = async (
signer: ethers.providers.JsonRpcSigner,
safeAddress: string,
sender: string,
{
baseGas = 0,
data,
gasPrice = 0,
gasToken = ZERO_ADDRESS,
operation = CALL,
refundReceiver = ZERO_ADDRESS,
safeTxGas = 0,
to,
valueInWei,
}: CreateTransactionArgs,
): Promise<{ safeTxHash: string; tx: ethers.ContractTransaction }> => {
const senderSignature = getPreValidatedSignature(sender);
const safeNonce = await getSafeNonce(signer, safeAddress);

const tx = {
baseGas,
data,
gasPrice,
gasToken,
operation,
refundReceiver,
safeTxGas,
nonce: safeNonce,
to,
valueInWei,
sigs: senderSignature,
};

const safeTxHash = await getTransactionHash(signer, safeAddress, tx);
const executedTx = await executeTransaction(signer, safeAddress, tx);

return { safeTxHash, tx: executedTx };
};

const encodeMultiSendCall = (
signer: ethers.providers.JsonRpcSigner,
multiSendAddress: string,
txs: Transaction[],
): string => {
const multiSend = getMultiSendContract(multiSendAddress, signer);

const joinedTxs = txs
.map((tx) => {
const data = ethers.utils.arrayify(tx.data);
const encoded = ethers.utils.solidityPack(
['uint8', 'address', 'uint256', 'uint256', 'bytes'],
[0, tx.to, tx.value, data.length, data],
);
return encoded.slice(2);
})
.join('');

const encodedMultiSendCallData = multiSend.interface.encodeFunctionData('multiSend', [`0x${joinedTxs}`]);

return encodedMultiSendCallData;
};

export { CALL, DELEGATE_CALL, encodeMultiSendCall, getTransactionHash, createTransaction };
11 changes: 11 additions & 0 deletions packages/safe-apps-developer-ui/src/assets/icons/icon-arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions packages/safe-apps-developer-ui/src/assets/icons/icon-code.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions packages/safe-apps-developer-ui/src/components/BalanceBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import styled from 'styled-components';
import { secondaryBackground, xs } from 'src/styles/variables';

const Box = styled.p`
background: ${secondaryBackground};
padding: ${xs};
border-radius: 5px;
width: fit-content;
span {
font-weight: bold;
}
`;

type Props = {
balance: string;
symbol?: string;
};

const BalanceBox = ({ balance, symbol = 'ETH' }: Props): React.ReactElement => (
<Box>
Balance:{' '}
<span>
{balance} {symbol}
</span>
</Box>
);

export { BalanceBox };
Loading

0 comments on commit f93eae4

Please sign in to comment.