Skip to content

Commit

Permalink
Sign, send and track transactions (#21)
Browse files Browse the repository at this point in the history
* Add transactions tracking
  • Loading branch information
arhtudormorar authored Sep 16, 2024
1 parent 333db0b commit b89cc1e
Show file tree
Hide file tree
Showing 59 changed files with 1,833 additions and 113 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]


- [Added sign, send, & track transactions with websocket connection](https://github.com/multiversx/mx-sdk-dapp-core/pull/21)
- [Added restore provider after page reload](https://github.com/multiversx/mx-sdk-dapp-core/pull/19)
- [Added signMessage](https://github.com/multiversx/mx-sdk-dapp-core/pull/18)

Expand Down
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,16 @@
"@lifeomic/axios-fetch": "3.0.1",
"@multiversx/sdk-extension-provider": "3.0.0",
"@multiversx/sdk-hw-provider": "6.4.0",
"@multiversx/sdk-metamask-provider": "0.0.5",
"@multiversx/sdk-metamask-provider": "0.0.7",
"@multiversx/sdk-native-auth-client": "^1.0.8",
"@multiversx/sdk-opera-provider": "1.0.0-alpha.1",
"@multiversx/sdk-wallet": "4.5.1",
"@multiversx/sdk-wallet-connect-provider": "4.1.2",
"@multiversx/sdk-web-wallet-provider": "3.2.1",
"@types/lodash": "^4.17.4",
"isomorphic-fetch": "^3.0.0",
"lodash": "^4.17.21",
"zustand": "^4.4.7"
"isomorphic-fetch": "3.0.0",
"lodash": "4.17.21",
"socket.io-client": "4.7.5",
"zustand": "4.4.7"
},
"peerDependencies": {
"@multiversx/sdk-core": ">= 13.0.0",
Expand All @@ -55,6 +55,7 @@
"string-width": "4.1.0"
},
"devDependencies": {
"@types/lodash": "4.17.4",
"@multiversx/sdk-core": ">= 13.0.0",
"@multiversx/sdk-dapp-utils": ">= 0.1.0",
"@multiversx/sdk-web-wallet-cross-window-provider": ">= 1.0.0",
Expand Down
File renamed without changes.
File renamed without changes.
16 changes: 16 additions & 0 deletions src/apiCalls/transactions/getTransactionByHash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import axios from 'axios';
import { TRANSACTIONS_ENDPOINT } from 'apiCalls/endpoints';
import { getState } from 'store/store';
import { networkSelector } from 'store/selectors';
import { ServerTransactionType } from 'types/serverTransactions.types';

export const getTransactionByHash = (hash: string) => {
const { apiAddress } = networkSelector(getState());

return axios.get<ServerTransactionType>(
`${apiAddress}/${TRANSACTIONS_ENDPOINT}/${hash}`,
{
timeout: 10000 // 10sec
}
);
};
44 changes: 44 additions & 0 deletions src/apiCalls/transactions/getTransactionsByHashes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import axios from 'axios';
import { TRANSACTIONS_ENDPOINT } from 'apiCalls/endpoints';

import { getState } from 'store/store';
import { networkSelector } from 'store/selectors';
import {
GetTransactionsByHashesReturnType,
PendingTransactionsType
} from 'types/transactions.types';

export const getTransactionsByHashes = async (
pendingTransactions: PendingTransactionsType
): Promise<GetTransactionsByHashesReturnType> => {
const { apiAddress } = networkSelector(getState());
const hashes = pendingTransactions.map((tx) => tx.hash);

const { data: responseData } = await axios.get(
`${apiAddress}/${TRANSACTIONS_ENDPOINT}`,
{
params: {
hashes: hashes.join(','),
withScResults: true
}
}
);

return pendingTransactions.map(({ hash, previousStatus }) => {
const txOnNetwork = responseData.find(
(txResponse: any) => txResponse?.txHash === hash
);

return {
hash,
data: txOnNetwork?.data,
invalidTransaction: txOnNetwork == null,
status: txOnNetwork?.status,
results: txOnNetwork?.results,
sender: txOnNetwork?.sender,
receiver: txOnNetwork?.receiver,
previousStatus,
hasStatusChanged: txOnNetwork && txOnNetwork.status !== previousStatus
};
});
};
13 changes: 13 additions & 0 deletions src/apiCalls/websocket/getWebsocketUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import axios from 'axios';

export async function getWebsocketUrl(apiAddress: string) {
try {
const { data } = await axios.get<{ url: string }>(
`${apiAddress}/websocket/config`
);
return `wss://${data.url}`;
} catch (err) {
console.error(err);
throw new Error('Can not get websocket url');
}
}
1 change: 1 addition & 0 deletions src/apiCalls/websocket/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './getWebsocketUrl';
2 changes: 1 addition & 1 deletion src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ export * from './storage.constants';
export * from './window.constants';
export * from './browser.constants';
export * from './errorMessages.constants';
export * from './ledger.constants';
export * from './mvx.constants';
1 change: 0 additions & 1 deletion src/constants/ledger.constants.ts

This file was deleted.

14 changes: 14 additions & 0 deletions src/constants/mvx.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const GAS_PRICE_MODIFIER = 0.01;
export const GAS_PER_DATA_BYTE = 1500;
export const GAS_LIMIT = 50000;
/**
* Extra gas limit for guarded transactions
*/
export const EXTRA_GAS_LIMIT_GUARDED_TX = 50000;
export const GAS_PRICE = 1000000000;
export const DECIMALS = 18;
export const DIGITS = 4;
export const VERSION = 1;
export const LEDGER_CONTRACT_DATA_ENABLED_VALUE = 1;
export const METACHAIN_SHARD_ID = 4294967295;
export const ALL_SHARDS_SHARD_ID = 4294967280;
9 changes: 6 additions & 3 deletions src/constants/network.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export const fallbackNetworkConfigurations: Record<
xAliasAddress: 'https://devnet.xalias.com',
apiAddress: 'https://devnet-api.multiversx.com',
explorerAddress: 'http://devnet-explorer.multiversx.com',
apiTimeout: '4000'
apiTimeout: '4000',
roundDuration: 6000
},
testnet: {
id: 'testnet',
Expand All @@ -39,7 +40,8 @@ export const fallbackNetworkConfigurations: Record<
xAliasAddress: 'https://testnet.xalias.com',
apiAddress: 'https://testnet-api.multiversx.com',
explorerAddress: 'http://testnet-explorer.multiversx.com',
apiTimeout: '4000'
apiTimeout: '4000',
roundDuration: 6000
},
mainnet: {
id: 'mainnet',
Expand All @@ -57,7 +59,8 @@ export const fallbackNetworkConfigurations: Record<
xAliasAddress: 'https://xalias.com',
apiAddress: 'https://api.multiversx.com',
explorerAddress: 'https://explorer.multiversx.com',
apiTimeout: '4000'
apiTimeout: '4000',
roundDuration: 6000
}
};

Expand Down
8 changes: 0 additions & 8 deletions src/constants/placeholders.ts

This file was deleted.

6 changes: 6 additions & 0 deletions src/constants/transactions.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const CANCEL_TRANSACTION_TOAST_ID = 'cancel-transaction-toast';
export const AVERAGE_TX_DURATION_MS = 6000;
export const CROSS_SHARD_ROUNDS = 5;
export const TRANSACTIONS_STATUS_POLLING_INTERVAL_MS = 90 * 1000; // 90sec
export const TRANSACTIONS_STATUS_DROP_INTERVAL_MS = 10 * 60 * 1000; // 10min
export const CANCEL_TRANSACTION_TOAST_DEFAULT_DURATION = 20000;
4 changes: 2 additions & 2 deletions src/core/methods/account/getAccount.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { accountSelector } from 'store/selectors';
import { getState } from 'store/store';

export function getAccount() {
return accountSelector(getState());
export function getAccount(state = getState()) {
return accountSelector(state);
}
10 changes: 10 additions & 0 deletions src/core/methods/initApp/initApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { getDefaultNativeAuthConfig } from 'services/nativeAuth/methods/getDefau
import { InitAppType } from './initApp.types';
import { getIsLoggedIn } from '../account/getIsLoggedIn';
import { restoreProvider } from 'core/providers/helpers/restoreProvider';
import { registerWebsocketListener } from './websocket/registerWebsocket';
import { trackTransactions } from '../trackTransactions/trackTransactions';

const defaultInitAppProps = {
storage: {
Expand All @@ -32,6 +34,9 @@ export const initApp = async ({
}: InitAppType) => {
initStore(storage.getStorageCallback);

const shouldEnableTransactionTracker =
dAppConfig.enableTansactionTracker !== false;

if (dAppConfig?.nativeAuth) {
const nativeAuthConfig: NativeAuthConfigType =
typeof dAppConfig.nativeAuth === 'boolean'
Expand All @@ -46,9 +51,14 @@ export const initApp = async ({
environment: dAppConfig.environment
});

if (shouldEnableTransactionTracker) {
trackTransactions();
}

const isLoggedIn = getIsLoggedIn();

if (isLoggedIn) {
await restoreProvider();
await registerWebsocketListener();
}
};
4 changes: 4 additions & 0 deletions src/core/methods/initApp/initApp.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ type BaseDappConfigType = {
* If set to `NativeAuthConfigType`, will set the native auth configuration.
*/
nativeAuth?: boolean | NativeAuthConfigType;
/**
* default: `true`
*/
enableTansactionTracker?: boolean;
};

export type EnvironmentDappConfigType = BaseDappConfigType & {
Expand Down
113 changes: 113 additions & 0 deletions src/core/methods/initApp/websocket/initializeWebsocketConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { io } from 'socket.io-client';
import { retryMultipleTimes } from 'utils/retryMultipleTimes';
import {
BatchTransactionsWSResponseType,
websocketConnection,
WebsocketConnectionStatusEnum
} from './websocket.constants';
import { getWebsocketUrl } from 'apiCalls/websocket';
import { getStore } from 'store/store';
import { getAccount } from 'core/methods/account/getAccount';
import { networkSelector } from 'store/selectors';
import {
setWebsocketBatchEvent,
setWebsocketEvent
} from 'store/actions/account/accountActions';

const TIMEOUT = 3000;
const RECONNECTION_ATTEMPTS = 3;
const RETRY_INTERVAL = 500;
const MESSAGE_DELAY = 1000;
const BATCH_UPDATED_EVENT = 'batchUpdated';
const CONNECT = 'connect';
const DISCONNECT = 'disconnect';

export async function initializeWebsocketConnection() {
const { address } = getAccount();
const { apiAddress } = networkSelector(getStore().getState());

let messageTimeout: NodeJS.Timeout | null = null;
let batchTimeout: NodeJS.Timeout | null = null;

const handleMessageReceived = (message: string) => {
if (messageTimeout) {
clearTimeout(messageTimeout);
}
messageTimeout = setTimeout(() => {
setWebsocketEvent(message);
}, MESSAGE_DELAY);
};

const handleBatchUpdate = (data: BatchTransactionsWSResponseType) => {
if (batchTimeout) {
clearTimeout(batchTimeout);
}
batchTimeout = setTimeout(() => {
setWebsocketBatchEvent(data);
}, MESSAGE_DELAY);
};

const initializeConnection = retryMultipleTimes(
async () => {
// To avoid multiple connections to the same endpoint
websocketConnection.status = WebsocketConnectionStatusEnum.PENDING;

const websocketUrl = await getWebsocketUrl(apiAddress);

if (!websocketUrl) {
console.warn('Cannot get websocket URL');
return;
}

websocketConnection.instance = io(websocketUrl, {
forceNew: true,
reconnectionAttempts: RECONNECTION_ATTEMPTS,
timeout: TIMEOUT,
query: { address }
});

websocketConnection.status = WebsocketConnectionStatusEnum.COMPLETED;

websocketConnection.instance.onAny(handleMessageReceived);

websocketConnection.instance.on(BATCH_UPDATED_EVENT, handleBatchUpdate);

websocketConnection.instance.on(CONNECT, () => {
console.log('Websocket connected.');
});

websocketConnection.instance.on(DISCONNECT, () => {
console.warn('Websocket disconnected. Trying to reconnect...');
setTimeout(() => {
console.log('Websocket reconnecting...');
websocketConnection.instance?.connect();
}, RETRY_INTERVAL);
});
},
{ retries: 2, delay: RETRY_INTERVAL }
);

const closeConnection = () => {
websocketConnection.instance?.close();
websocketConnection.status = WebsocketConnectionStatusEnum.NOT_INITIALIZED;
if (messageTimeout) {
clearTimeout(messageTimeout);
}
if (batchTimeout) {
clearTimeout(batchTimeout);
}
};

if (
address &&
websocketConnection.status ===
WebsocketConnectionStatusEnum.NOT_INITIALIZED &&
!websocketConnection.instance?.active
) {
await initializeConnection();
}

return {
closeConnection
};
}
25 changes: 25 additions & 0 deletions src/core/methods/initApp/websocket/registerWebsocket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { initializeWebsocketConnection } from './initializeWebsocketConnection';
import { getStore } from 'store/store';
import { getAccount } from 'core/methods/account/getAccount';

let localAddress = '';
let closeConnectionRef: () => void;

export const registerWebsocketListener = async () => {
const store = getStore();
const account = getAccount();
localAddress = account.address;

// Initialize the websocket connection
const data = await initializeWebsocketConnection();
closeConnectionRef = data.closeConnection;

store.subscribe(async ({ account: { address } }) => {
if (localAddress && address !== localAddress) {
closeConnectionRef();
localAddress = address;
const { closeConnection } = await initializeWebsocketConnection();
closeConnectionRef = closeConnection;
}
});
};
Loading

0 comments on commit b89cc1e

Please sign in to comment.