diff --git a/docker/colony-cdapp-dev-env-auth-proxy b/docker/colony-cdapp-dev-env-auth-proxy index 85a91593216..adbe1937ed9 100644 --- a/docker/colony-cdapp-dev-env-auth-proxy +++ b/docker/colony-cdapp-dev-env-auth-proxy @@ -1,6 +1,6 @@ FROM colony-cdapp-dev-env/base:latest -ENV AUTH_PROXY_HASH=4326771fb553e8afc70f7b3860825e80cc321eae +ENV AUTH_PROXY_HASH=1b8da32237859df08129bc90cc6907564e3b011e # Add dependencies from the host # Note: these are listed individually so that if they change, they won't affect diff --git a/src/apollo/apolloClient.ts b/src/apollo/apolloClient.ts index 17faa986808..74f4bfbf201 100644 --- a/src/apollo/apolloClient.ts +++ b/src/apollo/apolloClient.ts @@ -1,38 +1,61 @@ -import { ApolloClient, from } from '@apollo/client'; +import { ApolloClient, ApolloLink, HttpLink, from } from '@apollo/client'; import { createAuthLink, AUTH_TYPE } from 'aws-appsync-auth-link'; import { createSubscriptionHandshakeLink } from 'aws-appsync-subscription-link'; import cache from './cache/index.ts'; import removeTypenameLink from './removeTypenameLink.ts'; -const auth = { - type: AUTH_TYPE.API_KEY as const, - apiKey: 'da2-fakeApiId123456', -}; - -// const noAuth = { -// type: AUTH_TYPE.NONE as const, +// const auth = { +// type: AUTH_TYPE.API_KEY as const, +// apiKey: 'da2-fakeApiId123456', // }; -const authLink = createAuthLink({ - // url: `${ - // import.meta.env.AUTH_PROXY_ENDPOINT || 'http://localhost:3005' - // }/graphql`, - url: 'http://localhost:20002/graphql', +const noAuth = { + type: AUTH_TYPE.NONE as const, +}; + +const httpLink = createAuthLink({ + url: `${ + import.meta.env.AUTH_PROXY_ENDPOINT || 'http://localhost:3005' + }/graphql`, region: 'us-east-1', - auth, + // auth is handled by auth proxy + auth: noAuth, + // auth, }); const subscriptionLink = createSubscriptionHandshakeLink({ - url: 'http://localhost:20002/graphql', + url: `${ + import.meta.env.AUTH_PROXY_ENDPOINT || 'http://localhost:3005' + }/graphql`, region: 'us-east-1', - auth, + // auth is handled by auth proxy + auth: noAuth, + // auth, +}); + +// Middleware link for setting credentials to include (aws links do not do that!) +const authLink = new ApolloLink((operation, forward) => { + operation.setContext({ + // ensures cookies are sent with every request + credentials: 'include', + }); + + return forward(operation); }); +// const httpLink = new HttpLink({ +// uri: `${ +// import.meta.env.AUTH_PROXY_ENDPOINT || 'http://localhost:3005' +// }/graphql`, +// credentials: 'include', +// }); + const apolloClient = new ApolloClient({ - link: from([removeTypenameLink, authLink, subscriptionLink]), - connectToDevTools: true, cache, + connectToDevTools: true, + link: from([removeTypenameLink, authLink, httpLink, subscriptionLink]), + // link: from([removeTypenameLink, httpLink]), /* * @TODO Most likely we'll need to add resolvers here as well */ diff --git a/src/components/common/Extensions/UserNavigation/hooks.ts b/src/components/common/Extensions/UserNavigation/hooks.ts index 7d9466bc448..0552309ef17 100644 --- a/src/components/common/Extensions/UserNavigation/hooks.ts +++ b/src/components/common/Extensions/UserNavigation/hooks.ts @@ -19,6 +19,8 @@ import { groupBy, unionBy } from '~utils/lodash.ts'; export const TRANSACTION_LIST_PAGE_SIZE = 10; +// FIXME: delete all of this, this is weird + const sortAndGroupTransactions = (transactions: Transaction[]) => { // Create groups of transations which have 'em const groupedTransactions = groupBy(transactions, 'groupId'); @@ -119,6 +121,7 @@ const updateQuery = (prev, { fetchMoreResult }) => { }; }; +// FIXME: delete this (check all places where its being used) export const useGroupedTransactionsAndMessages = (): { transactionAndMessageGroups: TransactionOrMessageGroups; fetchMoreTransactions: () => void; diff --git a/src/components/v5/frame/ColonyHome/ColonyHome.tsx b/src/components/v5/frame/ColonyHome/ColonyHome.tsx index 242682b713a..f64cfe9ce00 100644 --- a/src/components/v5/frame/ColonyHome/ColonyHome.tsx +++ b/src/components/v5/frame/ColonyHome/ColonyHome.tsx @@ -17,6 +17,8 @@ import { setQueryParamOnUrl } from '~utils/urls.ts'; // import TmpAdvancedPayments from '~v5/payments/TmpAdvancedPayments.tsx'; import Link from '~v5/shared/Link/index.ts'; +import { useGroupedTransactions } from '../../../../state/useUserTransactions.ts'; + import Agreements from './partials/Agreements/index.ts'; import DashboardHeader from './partials/DashboardHeader/index.ts'; import LeaveColonyModal from './partials/LeaveColonyModal/index.ts'; @@ -34,6 +36,8 @@ const ColonyHome = () => { const teamsBreadcrumbs = useCreateTeamBreadcrumbs(); useSetPageBreadcrumbs(teamsBreadcrumbs); + const tx = useGroupedTransactions(); + console.log('tx', tx); return (
diff --git a/src/redux/sagas/utils/createActionMetadata.ts b/src/redux/sagas/utils/createActionMetadata.ts index 15105da403f..9472fe5074a 100644 --- a/src/redux/sagas/utils/createActionMetadata.ts +++ b/src/redux/sagas/utils/createActionMetadata.ts @@ -21,3 +21,23 @@ export function* createActionMetadataInDB(txHash: string, customTitle: string) { }, }); } + +export const createActionMetadataInDb__NEW = async ( + txHash: string, + customTitle: string, +) => { + const apolloClient = getContext(ContextModule.ApolloClient); + + return apolloClient.mutate< + CreateColonyActionMetadataMutation, + CreateColonyActionMetadataMutationVariables + >({ + mutation: CreateColonyActionMetadataDocument, + variables: { + input: { + id: txHash, + customTitle, + }, + }, + }); +}; diff --git a/src/redux/sagas/utils/getCanUserSendMetatransactions.ts b/src/redux/sagas/utils/getCanUserSendMetatransactions.ts index 755160ffe89..ac469ee67c8 100644 --- a/src/redux/sagas/utils/getCanUserSendMetatransactions.ts +++ b/src/redux/sagas/utils/getCanUserSendMetatransactions.ts @@ -31,8 +31,7 @@ export function* getCanUserSendMetatransactions() { }); const userHasMetatransactionEnabled = - data.getUserByAddress?.items[0]?.profile?.meta?.metatransactionsEnabled || - false; + !!data.getUserByAddress?.items[0]?.profile?.meta?.metatransactionsEnabled; return userHasMetatransactionEnabled; } diff --git a/src/state/ColonyManager.ts b/src/state/ColonyManager.ts new file mode 100644 index 00000000000..2b640185789 --- /dev/null +++ b/src/state/ColonyManager.ts @@ -0,0 +1,75 @@ +import { ColonyNetwork, type Colony, PinataAdapter } from '@colony/sdk'; + +import { ContextModule, getContext } from '~context/index.ts'; +import { Network } from '~types/network.ts'; +import { isFullWallet } from '~types/wallet.ts'; + +// FIXME: remove +const PINATA_JWT__TEMP = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySW5mb3JtYXRpb24iOnsiaWQiOiJlZWNiZWNhMi0wMmU3LTQ4OTYtYWMzMi03MDQyMjc3OGEyYjciLCJlbWFpbCI6ImNocmlzQGNvbG9ueS5pbyIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJwaW5fcG9saWN5Ijp7InJlZ2lvbnMiOlt7ImlkIjoiRlJBMSIsImRlc2lyZWRSZXBsaWNhdGlvbkNvdW50IjoxfV0sInZlcnNpb24iOjF9LCJtZmFfZW5hYmxlZCI6ZmFsc2UsInN0YXR1cyI6IkFDVElWRSJ9LCJhdXRoZW50aWNhdGlvblR5cGUiOiJzY29wZWRLZXkiLCJzY29wZWRLZXlLZXkiOiIxYTFjZmNlOTI3MzYzODI1Mjg5OSIsInNjb3BlZEtleVNlY3JldCI6IjhjZTExNTNlNTY1NjJlZDUyODJmZGU3MWYzNmYyNjhmYjU5MjAxZGI5ZmY0OWNlMTZkNTRjZGIyMDJjMWIwNGQiLCJpYXQiOjE3MTE0Nzc4Mzh9.I-aALCEpRZksUxJFaHOlk9kW03mnyyh74hXaskIfA-U`; + +class ColonyManager { + colonies: Map; + + colonyNetwork?: ColonyNetwork; + + constructor() { + this.colonies = new Map(); + } + + async getColonyNetwork() { + if (this.colonyNetwork) { + return this.colonyNetwork; + } + + const wallet = getContext(ContextModule.Wallet); + + if (!isFullWallet(wallet)) { + throw new Error('Background login not yet completed.'); + } + + const signer = wallet.ethersProvider.getSigner(); + + const reputationOracleUrl = import.meta.env.REPUTATION_ORACLE_ENDPOINT + ? new URL(import.meta.env.REPUTATION_ORACLE_ENDPOINT) + : new URL(`/reputation`, window.location.origin); + + if (import.meta.env.DEV && import.meta.env.NETWORK_ID === Network.Ganache) { + const ganacheAccountsUrl = new URL( + import.meta.env.VITE_NETWORK_FILES_ENDPOINT || 'http://localhost:3006', + ); + const fetchRes = await fetch( + `${ganacheAccountsUrl.href}etherrouter-address.json`, + ); + const { etherRouterAddress: customNetworkAddress } = + await fetchRes.json(); + + this.colonyNetwork = new ColonyNetwork(signer, { + customNetworkAddress, + reputationOracleEndpoint: reputationOracleUrl.href, + ipfsAdapter: new PinataAdapter(PINATA_JWT__TEMP), + }); + + return this.colonyNetwork; + } + + this.colonyNetwork = new ColonyNetwork(signer, { + reputationOracleEndpoint: reputationOracleUrl.href, + ipfsAdapter: new PinataAdapter(PINATA_JWT__TEMP), + }); + + return this.colonyNetwork; + } + + async getColony(colonyAddress: string) { + if (this.colonies.has(colonyAddress)) { + return this.colonies.get(colonyAddress); + } + + const colonyNetwork = await this.getColonyNetwork(); + const colony = await colonyNetwork.getColony(colonyAddress); + this.colonies.set(colonyAddress, colony); + return colony; + } +} + +export default ColonyManager; diff --git a/src/state/transactions.ts b/src/state/transactions.ts new file mode 100644 index 00000000000..26bfa2585f9 --- /dev/null +++ b/src/state/transactions.ts @@ -0,0 +1,37 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { immer } from 'zustand/middleware/immer'; + +import ColonyManager from './ColonyManager.ts'; + +export const colonyManager = new ColonyManager(); + +export enum TxStatus { + Created = 'CREATED', + Ready = 'READY', + Sent = 'SENT', + Mined = 'MINED', + Error = 'ERROR', +} + +interface Transaction { + id: string; + name: string; + group?: { + key: string; + index: number; + }; + status: TxStatus; +} + +export interface TransactionsState { + transactions: Array; +} + +export const useTransactionsStore = create()( + devtools( + immer(() => ({ + transactions: [], + })), + ), +); diff --git a/src/state/transactions/moveFunds.ts b/src/state/transactions/moveFunds.ts new file mode 100644 index 00000000000..effad9d69b2 --- /dev/null +++ b/src/state/transactions/moveFunds.ts @@ -0,0 +1,284 @@ +import { randomUUID } from 'crypto'; +import { type BigNumber } from 'ethers'; + +import { createActionMetadataInDb__NEW } from '~redux/sagas/utils/createActionMetadata.ts'; +import { type Domain } from '~types/graphql.ts'; +import { type Address } from '~types/index.ts'; + +import { + colonyManager, + useTransactionsStore, + TxStatus, +} from '../transactions.ts'; + +export const moveFunds = async ( + { + colonyAddress, + colonyName, + fromDomain, + toDomain, + amount, + tokenAddress, + annotationMessage, + customActionTitle, + }: { + colonyAddress: Address; + customActionTitle: string; + colonyName?: string; + tokenAddress: Address; + fromDomain: Domain; + toDomain: Domain; + amount: BigNumber; + annotationMessage?: string; + }, + { setTxHash, navigate }, +) => { + /* + * Validate the required values for the payment + */ + if (!fromDomain) { + throw new Error( + 'Source domain not set for moveFundsBetweenPots transaction', + ); + } + if (!toDomain) { + throw new Error( + 'Recipient domain not set for moveFundsBetweenPots transaction', + ); + } + if (!amount) { + throw new Error( + 'Payment amount not set for moveFundsBetweenPots transaction', + ); + } + if (!tokenAddress) { + throw new Error( + 'Payment token not set for moveFundsBetweenPots transaction', + ); + } + + const { nativeId: fromTeam } = fromDomain; + const { nativeId: toTeam } = toDomain; + + // setup batch id + const batchKey = 'moveFunds'; + + const moveFundsId = randomUUID(); + const annotateMoveFundsId = randomUUID(); + + useTransactionsStore.setState((state) => { + state.transactions.push({ + id: moveFundsId, + // We could use an enum here ORR maybe do something smart with colony SDK + name: 'moveFundsBetweenPots', + group: { + key: batchKey, + index: 0, + }, + status: TxStatus.Ready, + }); + state.transactions.push({ + id: annotateMoveFundsId, + // We could use an enum here ORR maybe do something smart with colony SDK + name: 'annotateTransaction', + group: { + key: batchKey, + index: 1, + }, + status: TxStatus.Created, + }); + }); + + const colony = await colonyManager.getColony(colonyAddress); + + if (!colony) { + throw new Error(`Colony with address ${colonyAddress} not found`); + } + + const [{ hash }, waitForMoveFunds] = await colony + .moveFundsToTeam(amount, toTeam, fromTeam, tokenAddress) + .tx() + .send(); + + useTransactionsStore.setState((state) => { + const tx = state.transactions.find(({ id }) => id === moveFundsId); + if (tx) { + tx.status = TxStatus.Ready; + } + }); + + await createActionMetadataInDb__NEW(hash, customActionTitle); + + if (annotationMessage) { + await colony + .annotateTransaction(hash, { annotationMsg: annotationMessage }) + .tx() + .mined(); + } + + // This is probably mined by now, just for good measure we'll wait for it + await waitForMoveFunds(); + + useTransactionsStore.setState((state) => { + const moveFundsTx = state.transactions.find(({ id }) => id === moveFundsId); + if (moveFundsTx) { + moveFundsTx.status = TxStatus.Mined; + } + const annotateTx = state.transactions.find( + ({ id }) => id === annotateMoveFundsId, + ); + if (annotateTx) { + annotateTx.status = TxStatus.Mined; + } + }); + + setTxHash?.(hash); + + if (colonyName && navigate) { + navigate(`/${colonyName}?tx=${hash}`, { + state: { isRedirect: true }, + }); + } +}; + +// function* createMoveFundsAction({ +// payload: { +// colonyAddress, +// colonyName, +// fromDomain, +// toDomain, +// amount, +// tokenAddress, +// annotationMessage, +// customActionTitle, +// }, +// meta: { id: metaId, navigate, setTxHash }, +// meta, +// }: Action) { +// let txChannel; +// try { +// /* +// * Validate the required values for the payment +// */ +// if (!fromDomain) { +// throw new Error( +// 'Source domain not set for moveFundsBetweenPots transaction', +// ); +// } +// if (!toDomain) { +// throw new Error( +// 'Recipient domain not set for moveFundsBetweenPots transaction', +// ); +// } +// if (!amount) { +// throw new Error( +// 'Payment amount not set for moveFundsBetweenPots transaction', +// ); +// } +// if (!tokenAddress) { +// throw new Error( +// 'Payment token not set for moveFundsBetweenPots transaction', +// ); +// } +// +// const { nativeFundingPotId: fromPot } = fromDomain; +// const { nativeFundingPotId: toPot } = toDomain; +// +// // setup batch id +// const batchKey = 'moveFunds'; +// +// yield fork(createTransaction, moveFunds.id, { +// context: ClientType.ColonyClient, +// methodName: +// 'moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,address)', +// identifier: colonyAddress, +// params: [], +// group: { +// key: batchKey, +// id: metaId, +// index: 0, +// }, +// ready: false, +// }); +// +// if (annotationMessage) { +// yield fork(createTransaction, annotateMoveFunds.id, { +// context: ClientType.ColonyClient, +// methodName: 'annotateTransaction', +// identifier: colonyAddress, +// params: [], +// group: { +// key: batchKey, +// id: metaId, +// index: 1, +// }, +// ready: false, +// }); +// } +// +// yield takeFrom(moveFunds.channel, ActionTypes.TRANSACTION_CREATED); +// +// if (annotationMessage) { +// yield takeFrom( +// annotateMoveFunds.channel, +// ActionTypes.TRANSACTION_CREATED, +// ); +// } +// +// yield put(transactionPending(moveFunds.id)); +// +// const [permissionDomainId, fromChildSkillIndex, toChildSkillIndex] = +// yield getMoveFundsPermissionProofs(colonyAddress, fromPot, toPot); +// +// yield put( +// transactionAddParams(moveFunds.id, [ +// permissionDomainId, +// fromChildSkillIndex, +// toChildSkillIndex, +// fromPot, +// toPot, +// amount, +// tokenAddress, +// ]), +// ); +// +// yield initiateTransaction({ id: moveFunds.id }); +// +// const { +// payload: { +// receipt: { transactionHash: txHash }, +// }, +// } = yield waitForTxResult(moveFunds.channel); +// +// yield createActionMetadataInDB(txHash, customActionTitle); +// +// if (annotationMessage) { +// yield uploadAnnotation({ +// txChannel: annotateMoveFunds, +// message: annotationMessage, +// txHash, +// }); +// } +// +// setTxHash?.(txHash); +// +// yield put({ +// type: ActionTypes.ACTION_MOVE_FUNDS_SUCCESS, +// meta, +// }); +// +// if (colonyName && navigate) { +// navigate(`/${colonyName}?tx=${txHash}`, { +// state: { isRedirect: true }, +// }); +// } +// } catch (caughtError) { +// yield putError(ActionTypes.ACTION_MOVE_FUNDS_ERROR, caughtError, meta); +// } finally { +// txChannel.close(); +// } +// } +// +// export default function* moveFundsActionSaga() { +// yield takeEvery(ActionTypes.ACTION_MOVE_FUNDS, createMoveFundsAction); +// } diff --git a/src/state/useUserTransactions.ts b/src/state/useUserTransactions.ts index 7fa759a4f97..2dfc3895737 100644 --- a/src/state/useUserTransactions.ts +++ b/src/state/useUserTransactions.ts @@ -20,6 +20,8 @@ export const useGroupedTransactions = () => { onData: ({ client, data }) => { const newTx = data?.data?.onCreateTransactionFrom; + console.log('new tx data', newTx); + if (!newTx) { return; } @@ -56,6 +58,8 @@ export const useGroupedTransactions = () => { skip: !user, }); + console.log(data); + const groupedAndSorted = useMemo(() => { const rawTransactions = data?.getTransactionsByUser?.items || []; return filter(