): JSX.Element | null {
+ const { network } = useNetworkContext();
+
+ // Do not destructure, some projects are not wrapped with ServiceProvider
+ const service = useServiceProviderContext();
+
+ const client = useMemo(
+ () => ({
+ whaleAPI: newWhaleAPIClient(newOceanOptions(network, service?.url)),
+ whaleRPC: newWhaleRpcClient(newOceanOptions(network, service?.url)),
+ }),
+ [network, service?.url],
+ );
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/walletkit/walletkit-ui/contexts/__snapshots__/LanguageProvider.test.tsx.snap b/walletkit/walletkit-ui/contexts/__snapshots__/LanguageProvider.test.tsx.snap
new file mode 100644
index 0000000000..66c47fdc98
--- /dev/null
+++ b/walletkit/walletkit-ui/contexts/__snapshots__/LanguageProvider.test.tsx.snap
@@ -0,0 +1,74 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`LanguageProvider Context test should match snapshot 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+ ,
+ "container": ,
+ "debug": [Function],
+ "findAllByAltText": [Function],
+ "findAllByDisplayValue": [Function],
+ "findAllByLabelText": [Function],
+ "findAllByPlaceholderText": [Function],
+ "findAllByRole": [Function],
+ "findAllByTestId": [Function],
+ "findAllByText": [Function],
+ "findAllByTitle": [Function],
+ "findByAltText": [Function],
+ "findByDisplayValue": [Function],
+ "findByLabelText": [Function],
+ "findByPlaceholderText": [Function],
+ "findByRole": [Function],
+ "findByTestId": [Function],
+ "findByText": [Function],
+ "findByTitle": [Function],
+ "getAllByAltText": [Function],
+ "getAllByDisplayValue": [Function],
+ "getAllByLabelText": [Function],
+ "getAllByPlaceholderText": [Function],
+ "getAllByRole": [Function],
+ "getAllByTestId": [Function],
+ "getAllByText": [Function],
+ "getAllByTitle": [Function],
+ "getByAltText": [Function],
+ "getByDisplayValue": [Function],
+ "getByLabelText": [Function],
+ "getByPlaceholderText": [Function],
+ "getByRole": [Function],
+ "getByTestId": [Function],
+ "getByText": [Function],
+ "getByTitle": [Function],
+ "queryAllByAltText": [Function],
+ "queryAllByDisplayValue": [Function],
+ "queryAllByLabelText": [Function],
+ "queryAllByPlaceholderText": [Function],
+ "queryAllByRole": [Function],
+ "queryAllByTestId": [Function],
+ "queryAllByText": [Function],
+ "queryAllByTitle": [Function],
+ "queryByAltText": [Function],
+ "queryByDisplayValue": [Function],
+ "queryByLabelText": [Function],
+ "queryByPlaceholderText": [Function],
+ "queryByRole": [Function],
+ "queryByTestId": [Function],
+ "queryByText": [Function],
+ "queryByTitle": [Function],
+ "rerender": [Function],
+ "unmount": [Function],
+}
+`;
diff --git a/walletkit/walletkit-ui/contexts/__snapshots__/ThemeProvider.test.tsx.snap b/walletkit/walletkit-ui/contexts/__snapshots__/ThemeProvider.test.tsx.snap
new file mode 100644
index 0000000000..534a97abe7
--- /dev/null
+++ b/walletkit/walletkit-ui/contexts/__snapshots__/ThemeProvider.test.tsx.snap
@@ -0,0 +1,80 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ThemeProvider Context test should match snapshot 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+ true
+
+
+ light
+
+
+
+ ,
+ "container":
+
+
+ true
+
+
+ light
+
+
+
,
+ "debug": [Function],
+ "findAllByAltText": [Function],
+ "findAllByDisplayValue": [Function],
+ "findAllByLabelText": [Function],
+ "findAllByPlaceholderText": [Function],
+ "findAllByRole": [Function],
+ "findAllByTestId": [Function],
+ "findAllByText": [Function],
+ "findAllByTitle": [Function],
+ "findByAltText": [Function],
+ "findByDisplayValue": [Function],
+ "findByLabelText": [Function],
+ "findByPlaceholderText": [Function],
+ "findByRole": [Function],
+ "findByTestId": [Function],
+ "findByText": [Function],
+ "findByTitle": [Function],
+ "getAllByAltText": [Function],
+ "getAllByDisplayValue": [Function],
+ "getAllByLabelText": [Function],
+ "getAllByPlaceholderText": [Function],
+ "getAllByRole": [Function],
+ "getAllByTestId": [Function],
+ "getAllByText": [Function],
+ "getAllByTitle": [Function],
+ "getByAltText": [Function],
+ "getByDisplayValue": [Function],
+ "getByLabelText": [Function],
+ "getByPlaceholderText": [Function],
+ "getByRole": [Function],
+ "getByTestId": [Function],
+ "getByText": [Function],
+ "getByTitle": [Function],
+ "queryAllByAltText": [Function],
+ "queryAllByDisplayValue": [Function],
+ "queryAllByLabelText": [Function],
+ "queryAllByPlaceholderText": [Function],
+ "queryAllByRole": [Function],
+ "queryAllByTestId": [Function],
+ "queryAllByText": [Function],
+ "queryAllByTitle": [Function],
+ "queryByAltText": [Function],
+ "queryByDisplayValue": [Function],
+ "queryByLabelText": [Function],
+ "queryByPlaceholderText": [Function],
+ "queryByRole": [Function],
+ "queryByTestId": [Function],
+ "queryByText": [Function],
+ "queryByTitle": [Function],
+ "rerender": [Function],
+ "unmount": [Function],
+}
+`;
diff --git a/walletkit/walletkit-ui/contexts/__snapshots__/WhaleContext.test.tsx.snap b/walletkit/walletkit-ui/contexts/__snapshots__/WhaleContext.test.tsx.snap
new file mode 100644
index 0000000000..a3cd9a7932
--- /dev/null
+++ b/walletkit/walletkit-ui/contexts/__snapshots__/WhaleContext.test.tsx.snap
@@ -0,0 +1,877 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Whale Context test Whale Context test for Changi should match snapshot 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+ {"url":"https://changi.ocean.jellyfishsdk.com","timeout":60000,"version":"v0","network":"changi"}
+
+
+
+ ,
+ "container":
+
+
+ {"url":"https://changi.ocean.jellyfishsdk.com","timeout":60000,"version":"v0","network":"changi"}
+
+
+
,
+ "debug": [Function],
+ "findAllByAltText": [Function],
+ "findAllByDisplayValue": [Function],
+ "findAllByLabelText": [Function],
+ "findAllByPlaceholderText": [Function],
+ "findAllByRole": [Function],
+ "findAllByTestId": [Function],
+ "findAllByText": [Function],
+ "findAllByTitle": [Function],
+ "findByAltText": [Function],
+ "findByDisplayValue": [Function],
+ "findByLabelText": [Function],
+ "findByPlaceholderText": [Function],
+ "findByRole": [Function],
+ "findByTestId": [Function],
+ "findByText": [Function],
+ "findByTitle": [Function],
+ "getAllByAltText": [Function],
+ "getAllByDisplayValue": [Function],
+ "getAllByLabelText": [Function],
+ "getAllByPlaceholderText": [Function],
+ "getAllByRole": [Function],
+ "getAllByTestId": [Function],
+ "getAllByText": [Function],
+ "getAllByTitle": [Function],
+ "getByAltText": [Function],
+ "getByDisplayValue": [Function],
+ "getByLabelText": [Function],
+ "getByPlaceholderText": [Function],
+ "getByRole": [Function],
+ "getByTestId": [Function],
+ "getByText": [Function],
+ "getByTitle": [Function],
+ "queryAllByAltText": [Function],
+ "queryAllByDisplayValue": [Function],
+ "queryAllByLabelText": [Function],
+ "queryAllByPlaceholderText": [Function],
+ "queryAllByRole": [Function],
+ "queryAllByTestId": [Function],
+ "queryAllByText": [Function],
+ "queryAllByTitle": [Function],
+ "queryByAltText": [Function],
+ "queryByDisplayValue": [Function],
+ "queryByLabelText": [Function],
+ "queryByPlaceholderText": [Function],
+ "queryByRole": [Function],
+ "queryByTestId": [Function],
+ "queryByText": [Function],
+ "queryByTitle": [Function],
+ "rerender": [Function],
+ "unmount": [Function],
+}
+`;
+
+exports[`Whale Context test Whale Context test for DevNet should match snapshot 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+ {"url":"http://devnet.ocean.jellyfishsdk.com","timeout":60000,"version":"v0","network":"devnet"}
+
+
+
+ ,
+ "container":
+
+
+ {"url":"http://devnet.ocean.jellyfishsdk.com","timeout":60000,"version":"v0","network":"devnet"}
+
+
+
,
+ "debug": [Function],
+ "findAllByAltText": [Function],
+ "findAllByDisplayValue": [Function],
+ "findAllByLabelText": [Function],
+ "findAllByPlaceholderText": [Function],
+ "findAllByRole": [Function],
+ "findAllByTestId": [Function],
+ "findAllByText": [Function],
+ "findAllByTitle": [Function],
+ "findByAltText": [Function],
+ "findByDisplayValue": [Function],
+ "findByLabelText": [Function],
+ "findByPlaceholderText": [Function],
+ "findByRole": [Function],
+ "findByTestId": [Function],
+ "findByText": [Function],
+ "findByTitle": [Function],
+ "getAllByAltText": [Function],
+ "getAllByDisplayValue": [Function],
+ "getAllByLabelText": [Function],
+ "getAllByPlaceholderText": [Function],
+ "getAllByRole": [Function],
+ "getAllByTestId": [Function],
+ "getAllByText": [Function],
+ "getAllByTitle": [Function],
+ "getByAltText": [Function],
+ "getByDisplayValue": [Function],
+ "getByLabelText": [Function],
+ "getByPlaceholderText": [Function],
+ "getByRole": [Function],
+ "getByTestId": [Function],
+ "getByText": [Function],
+ "getByTitle": [Function],
+ "queryAllByAltText": [Function],
+ "queryAllByDisplayValue": [Function],
+ "queryAllByLabelText": [Function],
+ "queryAllByPlaceholderText": [Function],
+ "queryAllByRole": [Function],
+ "queryAllByTestId": [Function],
+ "queryAllByText": [Function],
+ "queryAllByTitle": [Function],
+ "queryByAltText": [Function],
+ "queryByDisplayValue": [Function],
+ "queryByLabelText": [Function],
+ "queryByPlaceholderText": [Function],
+ "queryByRole": [Function],
+ "queryByTestId": [Function],
+ "queryByText": [Function],
+ "queryByTitle": [Function],
+ "rerender": [Function],
+ "unmount": [Function],
+}
+`;
+
+exports[`Whale Context test Whale Context test for Local should match snapshot 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+ {"url":"http://localhost:19553","timeout":60000,"version":"v0","network":"regtest"}
+
+
+
+ ,
+ "container":
+
+
+ {"url":"http://localhost:19553","timeout":60000,"version":"v0","network":"regtest"}
+
+
+
,
+ "debug": [Function],
+ "findAllByAltText": [Function],
+ "findAllByDisplayValue": [Function],
+ "findAllByLabelText": [Function],
+ "findAllByPlaceholderText": [Function],
+ "findAllByRole": [Function],
+ "findAllByTestId": [Function],
+ "findAllByText": [Function],
+ "findAllByTitle": [Function],
+ "findByAltText": [Function],
+ "findByDisplayValue": [Function],
+ "findByLabelText": [Function],
+ "findByPlaceholderText": [Function],
+ "findByRole": [Function],
+ "findByTestId": [Function],
+ "findByText": [Function],
+ "findByTitle": [Function],
+ "getAllByAltText": [Function],
+ "getAllByDisplayValue": [Function],
+ "getAllByLabelText": [Function],
+ "getAllByPlaceholderText": [Function],
+ "getAllByRole": [Function],
+ "getAllByTestId": [Function],
+ "getAllByText": [Function],
+ "getAllByTitle": [Function],
+ "getByAltText": [Function],
+ "getByDisplayValue": [Function],
+ "getByLabelText": [Function],
+ "getByPlaceholderText": [Function],
+ "getByRole": [Function],
+ "getByTestId": [Function],
+ "getByText": [Function],
+ "getByTitle": [Function],
+ "queryAllByAltText": [Function],
+ "queryAllByDisplayValue": [Function],
+ "queryAllByLabelText": [Function],
+ "queryAllByPlaceholderText": [Function],
+ "queryAllByRole": [Function],
+ "queryAllByTestId": [Function],
+ "queryAllByText": [Function],
+ "queryAllByTitle": [Function],
+ "queryByAltText": [Function],
+ "queryByDisplayValue": [Function],
+ "queryByLabelText": [Function],
+ "queryByPlaceholderText": [Function],
+ "queryByRole": [Function],
+ "queryByTestId": [Function],
+ "queryByText": [Function],
+ "queryByTitle": [Function],
+ "rerender": [Function],
+ "unmount": [Function],
+}
+`;
+
+exports[`Whale Context test Whale Context test for MainNet should match snapshot 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+ {"url":"https://ocean.defichain.com","timeout":60000,"version":"v0","network":"mainnet"}
+
+
+
+ ,
+ "container":
+
+
+ {"url":"https://ocean.defichain.com","timeout":60000,"version":"v0","network":"mainnet"}
+
+
+
,
+ "debug": [Function],
+ "findAllByAltText": [Function],
+ "findAllByDisplayValue": [Function],
+ "findAllByLabelText": [Function],
+ "findAllByPlaceholderText": [Function],
+ "findAllByRole": [Function],
+ "findAllByTestId": [Function],
+ "findAllByText": [Function],
+ "findAllByTitle": [Function],
+ "findByAltText": [Function],
+ "findByDisplayValue": [Function],
+ "findByLabelText": [Function],
+ "findByPlaceholderText": [Function],
+ "findByRole": [Function],
+ "findByTestId": [Function],
+ "findByText": [Function],
+ "findByTitle": [Function],
+ "getAllByAltText": [Function],
+ "getAllByDisplayValue": [Function],
+ "getAllByLabelText": [Function],
+ "getAllByPlaceholderText": [Function],
+ "getAllByRole": [Function],
+ "getAllByTestId": [Function],
+ "getAllByText": [Function],
+ "getAllByTitle": [Function],
+ "getByAltText": [Function],
+ "getByDisplayValue": [Function],
+ "getByLabelText": [Function],
+ "getByPlaceholderText": [Function],
+ "getByRole": [Function],
+ "getByTestId": [Function],
+ "getByText": [Function],
+ "getByTitle": [Function],
+ "queryAllByAltText": [Function],
+ "queryAllByDisplayValue": [Function],
+ "queryAllByLabelText": [Function],
+ "queryAllByPlaceholderText": [Function],
+ "queryAllByRole": [Function],
+ "queryAllByTestId": [Function],
+ "queryAllByText": [Function],
+ "queryAllByTitle": [Function],
+ "queryByAltText": [Function],
+ "queryByDisplayValue": [Function],
+ "queryByLabelText": [Function],
+ "queryByPlaceholderText": [Function],
+ "queryByRole": [Function],
+ "queryByTestId": [Function],
+ "queryByText": [Function],
+ "queryByTitle": [Function],
+ "rerender": [Function],
+ "unmount": [Function],
+}
+`;
+
+exports[`Whale Context test Whale Context test for Playground should match snapshot 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+ {"url":"https://playground.jellyfishsdk.com","timeout":60000,"version":"v0","network":"regtest"}
+
+
+
+ ,
+ "container":
+
+
+ {"url":"https://playground.jellyfishsdk.com","timeout":60000,"version":"v0","network":"regtest"}
+
+
+
,
+ "debug": [Function],
+ "findAllByAltText": [Function],
+ "findAllByDisplayValue": [Function],
+ "findAllByLabelText": [Function],
+ "findAllByPlaceholderText": [Function],
+ "findAllByRole": [Function],
+ "findAllByTestId": [Function],
+ "findAllByText": [Function],
+ "findAllByTitle": [Function],
+ "findByAltText": [Function],
+ "findByDisplayValue": [Function],
+ "findByLabelText": [Function],
+ "findByPlaceholderText": [Function],
+ "findByRole": [Function],
+ "findByTestId": [Function],
+ "findByText": [Function],
+ "findByTitle": [Function],
+ "getAllByAltText": [Function],
+ "getAllByDisplayValue": [Function],
+ "getAllByLabelText": [Function],
+ "getAllByPlaceholderText": [Function],
+ "getAllByRole": [Function],
+ "getAllByTestId": [Function],
+ "getAllByText": [Function],
+ "getAllByTitle": [Function],
+ "getByAltText": [Function],
+ "getByDisplayValue": [Function],
+ "getByLabelText": [Function],
+ "getByPlaceholderText": [Function],
+ "getByRole": [Function],
+ "getByTestId": [Function],
+ "getByText": [Function],
+ "getByTitle": [Function],
+ "queryAllByAltText": [Function],
+ "queryAllByDisplayValue": [Function],
+ "queryAllByLabelText": [Function],
+ "queryAllByPlaceholderText": [Function],
+ "queryAllByRole": [Function],
+ "queryAllByTestId": [Function],
+ "queryAllByText": [Function],
+ "queryAllByTitle": [Function],
+ "queryByAltText": [Function],
+ "queryByDisplayValue": [Function],
+ "queryByLabelText": [Function],
+ "queryByPlaceholderText": [Function],
+ "queryByRole": [Function],
+ "queryByTestId": [Function],
+ "queryByText": [Function],
+ "queryByTitle": [Function],
+ "rerender": [Function],
+ "unmount": [Function],
+}
+`;
+
+exports[`Whale Context test Whale Context test for TestNet should match snapshot 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+ {"url":"https://testnet.ocean.jellyfishsdk.com","timeout":60000,"version":"v0","network":"testnet"}
+
+
+
+ ,
+ "container":
+
+
+ {"url":"https://testnet.ocean.jellyfishsdk.com","timeout":60000,"version":"v0","network":"testnet"}
+
+
+
,
+ "debug": [Function],
+ "findAllByAltText": [Function],
+ "findAllByDisplayValue": [Function],
+ "findAllByLabelText": [Function],
+ "findAllByPlaceholderText": [Function],
+ "findAllByRole": [Function],
+ "findAllByTestId": [Function],
+ "findAllByText": [Function],
+ "findAllByTitle": [Function],
+ "findByAltText": [Function],
+ "findByDisplayValue": [Function],
+ "findByLabelText": [Function],
+ "findByPlaceholderText": [Function],
+ "findByRole": [Function],
+ "findByTestId": [Function],
+ "findByText": [Function],
+ "findByTitle": [Function],
+ "getAllByAltText": [Function],
+ "getAllByDisplayValue": [Function],
+ "getAllByLabelText": [Function],
+ "getAllByPlaceholderText": [Function],
+ "getAllByRole": [Function],
+ "getAllByTestId": [Function],
+ "getAllByText": [Function],
+ "getAllByTitle": [Function],
+ "getByAltText": [Function],
+ "getByDisplayValue": [Function],
+ "getByLabelText": [Function],
+ "getByPlaceholderText": [Function],
+ "getByRole": [Function],
+ "getByTestId": [Function],
+ "getByText": [Function],
+ "getByTitle": [Function],
+ "queryAllByAltText": [Function],
+ "queryAllByDisplayValue": [Function],
+ "queryAllByLabelText": [Function],
+ "queryAllByPlaceholderText": [Function],
+ "queryAllByRole": [Function],
+ "queryAllByTestId": [Function],
+ "queryAllByText": [Function],
+ "queryAllByTitle": [Function],
+ "queryByAltText": [Function],
+ "queryByDisplayValue": [Function],
+ "queryByLabelText": [Function],
+ "queryByPlaceholderText": [Function],
+ "queryByRole": [Function],
+ "queryByTestId": [Function],
+ "queryByText": [Function],
+ "queryByTitle": [Function],
+ "rerender": [Function],
+ "unmount": [Function],
+}
+`;
+
+exports[`Whale custom provider url test Whale custom provider url test for Changi should match custom provider url: https://custom.changi.com 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+ {"url":"https://custom.changi.com","timeout":60000,"version":"v0","network":"changi"}
+
+
+
+ ,
+ "container":
+
+
+ {"url":"https://custom.changi.com","timeout":60000,"version":"v0","network":"changi"}
+
+
+
,
+ "debug": [Function],
+ "findAllByAltText": [Function],
+ "findAllByDisplayValue": [Function],
+ "findAllByLabelText": [Function],
+ "findAllByPlaceholderText": [Function],
+ "findAllByRole": [Function],
+ "findAllByTestId": [Function],
+ "findAllByText": [Function],
+ "findAllByTitle": [Function],
+ "findByAltText": [Function],
+ "findByDisplayValue": [Function],
+ "findByLabelText": [Function],
+ "findByPlaceholderText": [Function],
+ "findByRole": [Function],
+ "findByTestId": [Function],
+ "findByText": [Function],
+ "findByTitle": [Function],
+ "getAllByAltText": [Function],
+ "getAllByDisplayValue": [Function],
+ "getAllByLabelText": [Function],
+ "getAllByPlaceholderText": [Function],
+ "getAllByRole": [Function],
+ "getAllByTestId": [Function],
+ "getAllByText": [Function],
+ "getAllByTitle": [Function],
+ "getByAltText": [Function],
+ "getByDisplayValue": [Function],
+ "getByLabelText": [Function],
+ "getByPlaceholderText": [Function],
+ "getByRole": [Function],
+ "getByTestId": [Function],
+ "getByText": [Function],
+ "getByTitle": [Function],
+ "queryAllByAltText": [Function],
+ "queryAllByDisplayValue": [Function],
+ "queryAllByLabelText": [Function],
+ "queryAllByPlaceholderText": [Function],
+ "queryAllByRole": [Function],
+ "queryAllByTestId": [Function],
+ "queryAllByText": [Function],
+ "queryAllByTitle": [Function],
+ "queryByAltText": [Function],
+ "queryByDisplayValue": [Function],
+ "queryByLabelText": [Function],
+ "queryByPlaceholderText": [Function],
+ "queryByRole": [Function],
+ "queryByTestId": [Function],
+ "queryByText": [Function],
+ "queryByTitle": [Function],
+ "rerender": [Function],
+ "unmount": [Function],
+}
+`;
+
+exports[`Whale custom provider url test Whale custom provider url test for DevNet should match custom provider url: https://custom.devnet.com 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+ {"url":"https://custom.devnet.com","timeout":60000,"version":"v0","network":"devnet"}
+
+
+
+ ,
+ "container":
+
+
+ {"url":"https://custom.devnet.com","timeout":60000,"version":"v0","network":"devnet"}
+
+
+
,
+ "debug": [Function],
+ "findAllByAltText": [Function],
+ "findAllByDisplayValue": [Function],
+ "findAllByLabelText": [Function],
+ "findAllByPlaceholderText": [Function],
+ "findAllByRole": [Function],
+ "findAllByTestId": [Function],
+ "findAllByText": [Function],
+ "findAllByTitle": [Function],
+ "findByAltText": [Function],
+ "findByDisplayValue": [Function],
+ "findByLabelText": [Function],
+ "findByPlaceholderText": [Function],
+ "findByRole": [Function],
+ "findByTestId": [Function],
+ "findByText": [Function],
+ "findByTitle": [Function],
+ "getAllByAltText": [Function],
+ "getAllByDisplayValue": [Function],
+ "getAllByLabelText": [Function],
+ "getAllByPlaceholderText": [Function],
+ "getAllByRole": [Function],
+ "getAllByTestId": [Function],
+ "getAllByText": [Function],
+ "getAllByTitle": [Function],
+ "getByAltText": [Function],
+ "getByDisplayValue": [Function],
+ "getByLabelText": [Function],
+ "getByPlaceholderText": [Function],
+ "getByRole": [Function],
+ "getByTestId": [Function],
+ "getByText": [Function],
+ "getByTitle": [Function],
+ "queryAllByAltText": [Function],
+ "queryAllByDisplayValue": [Function],
+ "queryAllByLabelText": [Function],
+ "queryAllByPlaceholderText": [Function],
+ "queryAllByRole": [Function],
+ "queryAllByTestId": [Function],
+ "queryAllByText": [Function],
+ "queryAllByTitle": [Function],
+ "queryByAltText": [Function],
+ "queryByDisplayValue": [Function],
+ "queryByLabelText": [Function],
+ "queryByPlaceholderText": [Function],
+ "queryByRole": [Function],
+ "queryByTestId": [Function],
+ "queryByText": [Function],
+ "queryByTitle": [Function],
+ "rerender": [Function],
+ "unmount": [Function],
+}
+`;
+
+exports[`Whale custom provider url test Whale custom provider url test for Local should match custom provider url: https://custom.local.com 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+ {"url":"https://custom.local.com","timeout":60000,"version":"v0","network":"regtest"}
+
+
+
+ ,
+ "container":
+
+
+ {"url":"https://custom.local.com","timeout":60000,"version":"v0","network":"regtest"}
+
+
+
,
+ "debug": [Function],
+ "findAllByAltText": [Function],
+ "findAllByDisplayValue": [Function],
+ "findAllByLabelText": [Function],
+ "findAllByPlaceholderText": [Function],
+ "findAllByRole": [Function],
+ "findAllByTestId": [Function],
+ "findAllByText": [Function],
+ "findAllByTitle": [Function],
+ "findByAltText": [Function],
+ "findByDisplayValue": [Function],
+ "findByLabelText": [Function],
+ "findByPlaceholderText": [Function],
+ "findByRole": [Function],
+ "findByTestId": [Function],
+ "findByText": [Function],
+ "findByTitle": [Function],
+ "getAllByAltText": [Function],
+ "getAllByDisplayValue": [Function],
+ "getAllByLabelText": [Function],
+ "getAllByPlaceholderText": [Function],
+ "getAllByRole": [Function],
+ "getAllByTestId": [Function],
+ "getAllByText": [Function],
+ "getAllByTitle": [Function],
+ "getByAltText": [Function],
+ "getByDisplayValue": [Function],
+ "getByLabelText": [Function],
+ "getByPlaceholderText": [Function],
+ "getByRole": [Function],
+ "getByTestId": [Function],
+ "getByText": [Function],
+ "getByTitle": [Function],
+ "queryAllByAltText": [Function],
+ "queryAllByDisplayValue": [Function],
+ "queryAllByLabelText": [Function],
+ "queryAllByPlaceholderText": [Function],
+ "queryAllByRole": [Function],
+ "queryAllByTestId": [Function],
+ "queryAllByText": [Function],
+ "queryAllByTitle": [Function],
+ "queryByAltText": [Function],
+ "queryByDisplayValue": [Function],
+ "queryByLabelText": [Function],
+ "queryByPlaceholderText": [Function],
+ "queryByRole": [Function],
+ "queryByTestId": [Function],
+ "queryByText": [Function],
+ "queryByTitle": [Function],
+ "rerender": [Function],
+ "unmount": [Function],
+}
+`;
+
+exports[`Whale custom provider url test Whale custom provider url test for MainNet should match custom provider url: https://custom.mainnet.com 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+ {"url":"https://custom.mainnet.com","timeout":60000,"version":"v0","network":"mainnet"}
+
+
+
+ ,
+ "container":
+
+
+ {"url":"https://custom.mainnet.com","timeout":60000,"version":"v0","network":"mainnet"}
+
+
+
,
+ "debug": [Function],
+ "findAllByAltText": [Function],
+ "findAllByDisplayValue": [Function],
+ "findAllByLabelText": [Function],
+ "findAllByPlaceholderText": [Function],
+ "findAllByRole": [Function],
+ "findAllByTestId": [Function],
+ "findAllByText": [Function],
+ "findAllByTitle": [Function],
+ "findByAltText": [Function],
+ "findByDisplayValue": [Function],
+ "findByLabelText": [Function],
+ "findByPlaceholderText": [Function],
+ "findByRole": [Function],
+ "findByTestId": [Function],
+ "findByText": [Function],
+ "findByTitle": [Function],
+ "getAllByAltText": [Function],
+ "getAllByDisplayValue": [Function],
+ "getAllByLabelText": [Function],
+ "getAllByPlaceholderText": [Function],
+ "getAllByRole": [Function],
+ "getAllByTestId": [Function],
+ "getAllByText": [Function],
+ "getAllByTitle": [Function],
+ "getByAltText": [Function],
+ "getByDisplayValue": [Function],
+ "getByLabelText": [Function],
+ "getByPlaceholderText": [Function],
+ "getByRole": [Function],
+ "getByTestId": [Function],
+ "getByText": [Function],
+ "getByTitle": [Function],
+ "queryAllByAltText": [Function],
+ "queryAllByDisplayValue": [Function],
+ "queryAllByLabelText": [Function],
+ "queryAllByPlaceholderText": [Function],
+ "queryAllByRole": [Function],
+ "queryAllByTestId": [Function],
+ "queryAllByText": [Function],
+ "queryAllByTitle": [Function],
+ "queryByAltText": [Function],
+ "queryByDisplayValue": [Function],
+ "queryByLabelText": [Function],
+ "queryByPlaceholderText": [Function],
+ "queryByRole": [Function],
+ "queryByTestId": [Function],
+ "queryByText": [Function],
+ "queryByTitle": [Function],
+ "rerender": [Function],
+ "unmount": [Function],
+}
+`;
+
+exports[`Whale custom provider url test Whale custom provider url test for Playground should match custom provider url: https://custom.playground.com 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+ {"url":"https://custom.playground.com","timeout":60000,"version":"v0","network":"regtest"}
+
+
+
+ ,
+ "container":
+
+
+ {"url":"https://custom.playground.com","timeout":60000,"version":"v0","network":"regtest"}
+
+
+
,
+ "debug": [Function],
+ "findAllByAltText": [Function],
+ "findAllByDisplayValue": [Function],
+ "findAllByLabelText": [Function],
+ "findAllByPlaceholderText": [Function],
+ "findAllByRole": [Function],
+ "findAllByTestId": [Function],
+ "findAllByText": [Function],
+ "findAllByTitle": [Function],
+ "findByAltText": [Function],
+ "findByDisplayValue": [Function],
+ "findByLabelText": [Function],
+ "findByPlaceholderText": [Function],
+ "findByRole": [Function],
+ "findByTestId": [Function],
+ "findByText": [Function],
+ "findByTitle": [Function],
+ "getAllByAltText": [Function],
+ "getAllByDisplayValue": [Function],
+ "getAllByLabelText": [Function],
+ "getAllByPlaceholderText": [Function],
+ "getAllByRole": [Function],
+ "getAllByTestId": [Function],
+ "getAllByText": [Function],
+ "getAllByTitle": [Function],
+ "getByAltText": [Function],
+ "getByDisplayValue": [Function],
+ "getByLabelText": [Function],
+ "getByPlaceholderText": [Function],
+ "getByRole": [Function],
+ "getByTestId": [Function],
+ "getByText": [Function],
+ "getByTitle": [Function],
+ "queryAllByAltText": [Function],
+ "queryAllByDisplayValue": [Function],
+ "queryAllByLabelText": [Function],
+ "queryAllByPlaceholderText": [Function],
+ "queryAllByRole": [Function],
+ "queryAllByTestId": [Function],
+ "queryAllByText": [Function],
+ "queryAllByTitle": [Function],
+ "queryByAltText": [Function],
+ "queryByDisplayValue": [Function],
+ "queryByLabelText": [Function],
+ "queryByPlaceholderText": [Function],
+ "queryByRole": [Function],
+ "queryByTestId": [Function],
+ "queryByText": [Function],
+ "queryByTitle": [Function],
+ "rerender": [Function],
+ "unmount": [Function],
+}
+`;
+
+exports[`Whale custom provider url test Whale custom provider url test for TestNet should match custom provider url: https://custom.testnet.com 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+ {"url":"https://custom.testnet.com","timeout":60000,"version":"v0","network":"testnet"}
+
+
+
+ ,
+ "container":
+
+
+ {"url":"https://custom.testnet.com","timeout":60000,"version":"v0","network":"testnet"}
+
+
+
,
+ "debug": [Function],
+ "findAllByAltText": [Function],
+ "findAllByDisplayValue": [Function],
+ "findAllByLabelText": [Function],
+ "findAllByPlaceholderText": [Function],
+ "findAllByRole": [Function],
+ "findAllByTestId": [Function],
+ "findAllByText": [Function],
+ "findAllByTitle": [Function],
+ "findByAltText": [Function],
+ "findByDisplayValue": [Function],
+ "findByLabelText": [Function],
+ "findByPlaceholderText": [Function],
+ "findByRole": [Function],
+ "findByTestId": [Function],
+ "findByText": [Function],
+ "findByTitle": [Function],
+ "getAllByAltText": [Function],
+ "getAllByDisplayValue": [Function],
+ "getAllByLabelText": [Function],
+ "getAllByPlaceholderText": [Function],
+ "getAllByRole": [Function],
+ "getAllByTestId": [Function],
+ "getAllByText": [Function],
+ "getAllByTitle": [Function],
+ "getByAltText": [Function],
+ "getByDisplayValue": [Function],
+ "getByLabelText": [Function],
+ "getByPlaceholderText": [Function],
+ "getByRole": [Function],
+ "getByTestId": [Function],
+ "getByText": [Function],
+ "getByTitle": [Function],
+ "queryAllByAltText": [Function],
+ "queryAllByDisplayValue": [Function],
+ "queryAllByLabelText": [Function],
+ "queryAllByPlaceholderText": [Function],
+ "queryAllByRole": [Function],
+ "queryAllByTestId": [Function],
+ "queryAllByText": [Function],
+ "queryAllByTitle": [Function],
+ "queryByAltText": [Function],
+ "queryByDisplayValue": [Function],
+ "queryByLabelText": [Function],
+ "queryByPlaceholderText": [Function],
+ "queryByRole": [Function],
+ "queryByTestId": [Function],
+ "queryByText": [Function],
+ "queryByTitle": [Function],
+ "rerender": [Function],
+ "unmount": [Function],
+}
+`;
diff --git a/walletkit/walletkit-ui/contexts/index.ts b/walletkit/walletkit-ui/contexts/index.ts
new file mode 100644
index 0000000000..ccf79e774a
--- /dev/null
+++ b/walletkit/walletkit-ui/contexts/index.ts
@@ -0,0 +1,12 @@
+export * from "./DeFiScanContext";
+export * from "./DisplayBalanceContext";
+export * from "./LanguageProvider";
+export * from "./NetworkContext";
+export * from "./PlaygroundContext";
+export * from "./StatsProvider";
+export * from "./StoreProvider";
+export * from "./StoreServiceProvider";
+export * from "./ThemeProvider";
+export * from "./WalletNodeProvider";
+export * from "./WalletPersistenceContext";
+export * from "./WhaleContext";
diff --git a/walletkit/walletkit-ui/contexts/logger.ts b/walletkit/walletkit-ui/contexts/logger.ts
new file mode 100644
index 0000000000..5d77cbc6e0
--- /dev/null
+++ b/walletkit/walletkit-ui/contexts/logger.ts
@@ -0,0 +1,4 @@
+export interface BaseLogger {
+ error: (error: any) => void;
+ info: (message: string) => void;
+}
diff --git a/walletkit/walletkit-ui/data/exchanges.ts b/walletkit/walletkit-ui/data/exchanges.ts
new file mode 100644
index 0000000000..3ceb89d28a
--- /dev/null
+++ b/walletkit/walletkit-ui/data/exchanges.ts
@@ -0,0 +1,59 @@
+export interface ExchangeProps {
+ name: string;
+ url: string;
+}
+
+export const exchanges: ExchangeProps[] = [
+ {
+ name: "Kucoin",
+ url: "https://www.kucoin.com/trade/DFI-BTC",
+ },
+ {
+ name: "Huobi",
+ url: "https://www.huobi.com/en-us/exchange/dfi_usdt",
+ },
+ {
+ name: "Gate.io",
+ url: "https://www.gate.io/trade/DFI_USDT",
+ },
+ {
+ name: "Bittrex",
+ url: "https://global.bittrex.com/Market/Index?MarketName=BTC-DFI",
+ },
+ {
+ name: "Bitrue",
+ url: "https://www.bitrue.com/trade/dfi_btc",
+ },
+ {
+ name: "Latoken",
+ url: "https://latoken.com/exchange/DFI_BTC",
+ },
+ {
+ name: "DFX",
+ url: "https://dfx.swiss/en/",
+ },
+ {
+ name: "Transak",
+ url: "https://global.transak.com/",
+ },
+ {
+ name: "EasyCrypto (Australia)",
+ url: "https://easycrypto.com/au/buy-sell/dfi-defichain",
+ },
+ {
+ name: "EasyCrypto (New Zealand)",
+ url: "https://easycrypto.com/nz/buy-sell/dfi-defichain",
+ },
+ {
+ name: "Bybit",
+ url: "https://www.bybit.com/en-US/trade/spot/DFI/USDT",
+ },
+ {
+ name: "Swyftx",
+ url: "https://swyftx.com/au/buy/defichain/",
+ },
+ {
+ name: "Cake DeFi",
+ url: "https://cakedefi.com/",
+ },
+];
diff --git a/walletkit/walletkit-ui/data/index.ts b/walletkit/walletkit-ui/data/index.ts
new file mode 100644
index 0000000000..0c5f39eff9
--- /dev/null
+++ b/walletkit/walletkit-ui/data/index.ts
@@ -0,0 +1 @@
+export * from "./exchanges";
diff --git a/walletkit/walletkit-ui/hooks/index.ts b/walletkit/walletkit-ui/hooks/index.ts
new file mode 100644
index 0000000000..21f6f430ac
--- /dev/null
+++ b/walletkit/walletkit-ui/hooks/index.ts
@@ -0,0 +1,2 @@
+export * from "./useCollateralizationRatio";
+export * from "./useVaultStatus";
diff --git a/walletkit/walletkit-ui/hooks/useCollateralizationRatio.tsx b/walletkit/walletkit-ui/hooks/useCollateralizationRatio.tsx
new file mode 100644
index 0000000000..e51c560034
--- /dev/null
+++ b/walletkit/walletkit-ui/hooks/useCollateralizationRatio.tsx
@@ -0,0 +1,197 @@
+/* eslint-disable @typescript-eslint/no-shadow */
+import BigNumber from "bignumber.js";
+
+import {
+ CollateralizationRatioProps,
+ CollateralizationRatioStats,
+ VaultStatus,
+} from "../store/types/VaultStatus";
+
+export function useCollateralRatioStats({
+ colRatio,
+ minColRatio,
+ totalLoanAmount,
+ totalCollateralValue,
+}: CollateralizationRatioProps): CollateralizationRatioStats {
+ const atRiskThreshold = new BigNumber(minColRatio).multipliedBy(1.5);
+ const liquidatedThreshold = new BigNumber(minColRatio).multipliedBy(1.25);
+ const isInLiquidation =
+ totalLoanAmount.gt(0) && colRatio.isLessThan(liquidatedThreshold);
+ const isAtRisk =
+ totalLoanAmount.gt(0) && colRatio.isLessThan(atRiskThreshold);
+ return {
+ atRiskThreshold,
+ liquidatedThreshold,
+ isInLiquidation,
+ isAtRisk,
+ isHealthy: !isInLiquidation && !isAtRisk && totalLoanAmount.gt(0),
+ isReady:
+ !isInLiquidation &&
+ !isAtRisk &&
+ totalLoanAmount.eq(0) &&
+ totalCollateralValue !== undefined &&
+ totalCollateralValue.gt(0),
+ };
+}
+
+// export function useCollateralizationRatioColor(
+// props: CollateralizationRatioProps
+// ): ThemedProps {
+// const style: ThemedProps = {};
+// const stats = useCollateralRatioStats(props);
+
+// if (stats.isInLiquidation) {
+// style.light = tailwind("text-error-500");
+// style.dark = tailwind("text-darkerror-500");
+// } else if (stats.isAtRisk) {
+// style.light = tailwind("text-warning-500");
+// style.dark = tailwind("text-darkwarning-500");
+// } else if (stats.isHealthy) {
+// style.light = tailwind("text-success-500");
+// style.dark = tailwind("text-darksuccess-500");
+// }
+// return style;
+// }
+
+// export function getVaultStatusColor(
+// status: string,
+// isLight: boolean,
+// isText: boolean = false
+// ): string {
+// if (status === VaultStatus.NearLiquidation) {
+// return isText ? "text-red-v2" : getColor("red-v2");
+// } else if (status === VaultStatus.AtRisk) {
+// return isText ? "text-orange-v2" : getColor("orange-v2");
+// } else if (status === VaultStatus.Healthy || status === VaultStatus.Ready) {
+// return isText ? "text-green-v2" : getColor("green-v2");
+// }
+// return isText
+// ? isLight
+// ? "text-mono-light-v2-500"
+// : "text-mono-dark-v2-500"
+// : getColor(isLight ? "mono-light-v2-300" : "mono-dark-v2-300");
+// }
+
+export function getVaultStatusText(status: string): string {
+ switch (status) {
+ case VaultStatus.Ready:
+ return "Ready";
+ case VaultStatus.Halted:
+ return "Halted";
+ default:
+ return "Empty";
+ }
+}
+
+export function useResultingCollateralizationRatioByCollateral({
+ collateralValue,
+ collateralRatio,
+ minCollateralRatio,
+ totalLoanAmount,
+ numOfColorBars = 6,
+ totalCollateralValueInUSD,
+}: {
+ collateralValue: string;
+ collateralRatio: BigNumber;
+ minCollateralRatio: BigNumber;
+ totalLoanAmount: BigNumber;
+ totalCollateralValue?: BigNumber;
+ numOfColorBars?: number;
+ totalCollateralValueInUSD: BigNumber;
+}): {
+ resultingColRatio: BigNumber;
+ displayedColorBars: number;
+} {
+ const hasCollateralRatio =
+ !new BigNumber(collateralRatio).isNaN() &&
+ new BigNumber(collateralRatio).isPositive();
+ const resultingColRatio =
+ collateralValue === "" ||
+ !hasCollateralRatio ||
+ new BigNumber(collateralValue).isZero()
+ ? new BigNumber(collateralRatio)
+ : totalCollateralValueInUSD.dividedBy(totalLoanAmount).multipliedBy(100);
+
+ const numOfColorBarPerStatus = numOfColorBars / 3; // (3): liquidation, at risk, healthy
+ const healthyThresholdRatio = 1.75;
+ const atRiskThresholdRatio = 1.5;
+ const liquidatedThresholdRatio = 1.25;
+ const atRiskThreshold = new BigNumber(minCollateralRatio).multipliedBy(
+ atRiskThresholdRatio,
+ );
+ const liquidatedThreshold = new BigNumber(minCollateralRatio).multipliedBy(
+ liquidatedThresholdRatio,
+ );
+
+ const isAtRisk =
+ totalLoanAmount.gt(0) && resultingColRatio.isLessThan(atRiskThreshold);
+ const isInLiquidation =
+ totalLoanAmount.gt(0) && resultingColRatio.isLessThan(liquidatedThreshold);
+ const isHealthy = !isInLiquidation && !isAtRisk && totalLoanAmount.gt(0);
+
+ const getRatio = (): number => {
+ if (isHealthy) {
+ return healthyThresholdRatio;
+ }
+ if (isAtRisk && !isInLiquidation) {
+ return atRiskThresholdRatio;
+ }
+ return liquidatedThresholdRatio;
+ };
+
+ const getColorBarsCount = (
+ numOfColorBarPerStatus: number,
+ minCollateralRatio: BigNumber,
+ resultingCollateralRatio: BigNumber,
+ thresholdRatio: number,
+ isHealthy: boolean,
+ ): number => {
+ let colorBarsCount = -1;
+ let index = 1;
+ while (colorBarsCount === -1 && index <= numOfColorBarPerStatus) {
+ const colorBarMaxAmount = minCollateralRatio.plus(
+ minCollateralRatio.multipliedBy(
+ new BigNumber(thresholdRatio)
+ .minus(1)
+ .dividedBy(isHealthy ? 1 : numOfColorBarPerStatus)
+ .times(index), // divide threshold to number of bars
+ ),
+ );
+
+ if (resultingCollateralRatio.isLessThanOrEqualTo(colorBarMaxAmount)) {
+ colorBarsCount = index;
+ }
+
+ index += 1;
+ }
+
+ return colorBarsCount;
+ };
+
+ const colorBarsCount = getColorBarsCount(
+ numOfColorBarPerStatus,
+ minCollateralRatio,
+ resultingColRatio,
+ getRatio(),
+ isHealthy,
+ );
+
+ let displayedColorBars = -1;
+
+ if (resultingColRatio.isLessThanOrEqualTo(0)) {
+ displayedColorBars = -1;
+ } else if (isHealthy && colorBarsCount > 0) {
+ displayedColorBars = colorBarsCount + numOfColorBarPerStatus * 2;
+ } else if (isHealthy && colorBarsCount === -1) {
+ displayedColorBars = numOfColorBars; // display full color bar
+ } else if (isAtRisk && !isInLiquidation) {
+ displayedColorBars = colorBarsCount + numOfColorBarPerStatus;
+ } else {
+ displayedColorBars = colorBarsCount;
+ }
+
+ return {
+ displayedColorBars,
+ resultingColRatio,
+ };
+}
diff --git a/walletkit/walletkit-ui/hooks/useVaultStatus.tsx b/walletkit/walletkit-ui/hooks/useVaultStatus.tsx
new file mode 100644
index 0000000000..1ba34e4959
--- /dev/null
+++ b/walletkit/walletkit-ui/hooks/useVaultStatus.tsx
@@ -0,0 +1,43 @@
+import { LoanVaultState } from "@defichain/whale-api-client/dist/api/loan";
+import BigNumber from "bignumber.js";
+
+import { VaultHealthItem, VaultStatus } from "../store/types/VaultStatus";
+import { useCollateralRatioStats } from "./useCollateralizationRatio";
+
+export function useVaultStatus(
+ status: LoanVaultState | undefined,
+ collateralRatio: BigNumber,
+ minColRatio: BigNumber,
+ totalLoanAmount: BigNumber,
+ totalCollateralValue: BigNumber,
+): VaultHealthItem {
+ const colRatio = collateralRatio.gte(0) ? collateralRatio : new BigNumber(0);
+ const stats = useCollateralRatioStats({
+ colRatio,
+ minColRatio,
+ totalLoanAmount,
+ totalCollateralValue,
+ });
+ let vaultStatus: VaultStatus;
+ if (status === LoanVaultState.FROZEN) {
+ vaultStatus = VaultStatus.Halted;
+ } else if (status === LoanVaultState.UNKNOWN) {
+ vaultStatus = VaultStatus.Unknown;
+ } else if (status === LoanVaultState.IN_LIQUIDATION) {
+ vaultStatus = VaultStatus.Liquidated;
+ } else if (stats.isInLiquidation) {
+ vaultStatus = VaultStatus.NearLiquidation;
+ } else if (stats.isAtRisk) {
+ vaultStatus = VaultStatus.AtRisk;
+ } else if (stats.isHealthy) {
+ vaultStatus = VaultStatus.Healthy;
+ } else if (stats.isReady) {
+ vaultStatus = VaultStatus.Ready;
+ } else {
+ vaultStatus = VaultStatus.Empty;
+ }
+ return {
+ status: vaultStatus,
+ vaultStats: stats,
+ };
+}
diff --git a/walletkit/walletkit-ui/images.d.ts b/walletkit/walletkit-ui/images.d.ts
new file mode 100644
index 0000000000..f772359642
--- /dev/null
+++ b/walletkit/walletkit-ui/images.d.ts
@@ -0,0 +1,4 @@
+declare module "*.png" {
+ const src: string;
+ export default src;
+}
diff --git a/walletkit/walletkit-ui/index.ts b/walletkit/walletkit-ui/index.ts
new file mode 100644
index 0000000000..f27bee26b1
--- /dev/null
+++ b/walletkit/walletkit-ui/index.ts
@@ -0,0 +1,4 @@
+export * from "./contexts";
+export * from "./data";
+export * from "./hooks";
+export * from "./store";
diff --git a/walletkit/walletkit-ui/store/auctions.ts b/walletkit/walletkit-ui/store/auctions.ts
new file mode 100644
index 0000000000..ab4ff2318d
--- /dev/null
+++ b/walletkit/walletkit-ui/store/auctions.ts
@@ -0,0 +1,108 @@
+import { WhaleApiClient } from "@defichain/whale-api-client";
+import {
+ LoanVaultLiquidated,
+ LoanVaultLiquidationBatch,
+ VaultAuctionBatchHistory,
+} from "@defichain/whale-api-client/dist/api/loan";
+import {
+ createAsyncThunk,
+ createSelector,
+ createSlice,
+ PayloadAction,
+} from "@reduxjs/toolkit";
+
+export interface AuctionsState {
+ auctions: LoanVaultLiquidated[];
+ hasFetchAuctionsData: boolean;
+ bidHistory: VaultAuctionBatchHistory[];
+}
+
+export interface AuctionBatchProps extends LoanVaultLiquidationBatch {
+ auction: LoanVaultLiquidated;
+ collateralTokenSymbols: string[];
+}
+
+const initialState: AuctionsState = {
+ auctions: [],
+ hasFetchAuctionsData: false,
+ bidHistory: [],
+};
+
+export const fetchAuctions = createAsyncThunk(
+ "wallet/fetchAuctions",
+ async ({ size = 200, client }: { size?: number; client: WhaleApiClient }) =>
+ client.loan.listAuction(size),
+);
+
+export const fetchBidHistory = createAsyncThunk(
+ "wallet/fetchBidHistory",
+ async ({
+ vaultId,
+ liquidationHeight,
+ batchIndex,
+ client,
+ size = 200,
+ }: {
+ vaultId: string;
+ liquidationHeight: number;
+ batchIndex: number;
+ client: WhaleApiClient;
+ size: number;
+ }) =>
+ client.loan.listVaultAuctionHistory(
+ vaultId,
+ liquidationHeight,
+ batchIndex,
+ size,
+ ),
+);
+
+export const auctions = createSlice({
+ name: "auctions",
+ initialState,
+ reducers: {
+ resetBidHistory: (state) => {
+ state.bidHistory = [];
+ },
+ },
+ extraReducers: (builder) => {
+ builder.addCase(
+ fetchAuctions.fulfilled,
+ (state, action: PayloadAction) => {
+ state.auctions = action.payload;
+ state.hasFetchAuctionsData = true;
+ },
+ );
+ builder.addCase(
+ fetchBidHistory.fulfilled,
+ (state, action: PayloadAction) => {
+ state.bidHistory = action.payload;
+ },
+ );
+ },
+});
+
+/**
+ * Flattens the auctions -> batch
+ * Sorts by liquidation height
+ */
+export const getAuctionBatches = createSelector(
+ [(state: AuctionsState) => state.auctions],
+ (auctionsState) =>
+ auctionsState.reduce(
+ (auctionBatches, auction): AuctionBatchProps[] => {
+ const filteredAuctionBatches = auctionBatches;
+ auction.batches.forEach((batch) => {
+ filteredAuctionBatches.push({
+ ...batch,
+ auction,
+ collateralTokenSymbols: batch.collaterals.map(
+ ({ displaySymbol }) => displaySymbol,
+ ),
+ });
+ });
+ return filteredAuctionBatches;
+ },
+ [],
+ ),
+);
diff --git a/walletkit/walletkit-ui/store/auctions.unit.ts b/walletkit/walletkit-ui/store/auctions.unit.ts
new file mode 100644
index 0000000000..cfb224044e
--- /dev/null
+++ b/walletkit/walletkit-ui/store/auctions.unit.ts
@@ -0,0 +1,253 @@
+import {
+ LoanVaultLiquidated,
+ LoanVaultLiquidationBatch,
+ LoanVaultState,
+ VaultAuctionBatchHistory,
+} from "@defichain/whale-api-client/dist/api/loan";
+
+import {
+ auctions,
+ AuctionsState,
+ fetchAuctions,
+ fetchBidHistory,
+ getAuctionBatches,
+} from "./auctions";
+
+describe("auctions reducer", () => {
+ let initialState: AuctionsState;
+ const loanBatchFoo: LoanVaultLiquidationBatch = {
+ index: 1,
+ collaterals: [
+ {
+ id: "0",
+ amount: "0.16199027",
+ symbol: "DFI",
+ symbolKey: "DFI",
+ name: "Default Defi token",
+ displaySymbol: "DFI",
+ activePrice: {
+ id: "DFI-USD-1776",
+ key: "DFI-USD",
+ isLive: true,
+ block: {
+ hash: "478bf1d2322aaa010bb1b4575491a6b3594ec294cd9cb2e406a82e56c3315062",
+ height: 1776,
+ medianTime: 1641219432,
+ time: 1641219438,
+ },
+ active: {
+ amount: "100.00000000",
+ weightage: 3,
+ oracles: {
+ active: 3,
+ total: 3,
+ },
+ },
+ next: {
+ amount: "100.00000000",
+ weightage: 3,
+ oracles: {
+ active: 3,
+ total: 3,
+ },
+ },
+ sort: "000006f0",
+ },
+ },
+ ],
+ loan: {
+ id: "14",
+ amount: "1.00043890",
+ symbol: "TU10",
+ symbolKey: "TU10",
+ name: "Decentralized TU10",
+ displaySymbol: "dTU10",
+ activePrice: {
+ id: "TU10-USD-1776",
+ key: "TU10-USD",
+ isLive: true,
+ block: {
+ hash: "478bf1d2322aaa010bb1b4575491a6b3594ec294cd9cb2e406a82e56c3315062",
+ height: 1776,
+ medianTime: 1641219432,
+ time: 1641219438,
+ },
+ active: {
+ amount: "10.85668461",
+ weightage: 3,
+ oracles: {
+ active: 3,
+ total: 3,
+ },
+ },
+ next: {
+ amount: "10.85994194",
+ weightage: 3,
+ oracles: {
+ active: 3,
+ total: 3,
+ },
+ },
+ sort: "000006f0",
+ },
+ },
+ froms: [],
+ };
+ const loanBatchBar: LoanVaultLiquidationBatch = {
+ index: 0,
+ collaterals: [
+ {
+ id: "0",
+ amount: "0.83800973",
+ symbol: "DFI",
+ symbolKey: "DFI",
+ name: "Default Defi token",
+ displaySymbol: "DFI",
+ activePrice: {
+ id: "DFI-USD-1776",
+ key: "DFI-USD",
+ isLive: true,
+ block: {
+ hash: "478bf1d2322aaa010bb1b4575491a6b3594ec294cd9cb2e406a82e56c3315062",
+ height: 1776,
+ medianTime: 1641219432,
+ time: 1641219438,
+ },
+ active: {
+ amount: "100.00000000",
+ weightage: 3,
+ oracles: {
+ active: 3,
+ total: 3,
+ },
+ },
+ next: {
+ amount: "100.00000000",
+ weightage: 3,
+ oracles: {
+ active: 3,
+ total: 3,
+ },
+ },
+ sort: "000006f0",
+ },
+ },
+ ],
+ loan: {
+ id: "11",
+ amount: "56.13801760",
+ symbol: "DUSD",
+ symbolKey: "DUSD",
+ name: "Decentralized USD",
+ displaySymbol: "DUSD",
+ },
+ froms: [],
+ };
+ const liquidatedVaults: LoanVaultLiquidated[] = [
+ {
+ vaultId:
+ "0336a6e1bfeef859815f3e6c63595c6c0bc0db43d548df59237446ae041d3e00",
+ loanScheme: {
+ id: "MIN150",
+ minColRatio: "150",
+ interestRate: "5",
+ },
+ ownerAddress: "bcrt1q39r84tmh4xp7wmg32tnza8j544lynknvy8q2nr",
+ state: LoanVaultState.IN_LIQUIDATION,
+ batchCount: 1,
+ liquidationHeight: 1800,
+ liquidationPenalty: 5,
+ batches: [loanBatchFoo],
+ },
+ {
+ vaultId:
+ "3f736e4ea13b91ac7dd3633e30a04553570428ad3d1ea8e39ba3162c24ab0b14",
+ loanScheme: {
+ id: "MIN150",
+ minColRatio: "150",
+ interestRate: "5",
+ },
+ ownerAddress: "bcrt1q39r84tmh4xp7wmg32tnza8j544lynknvy8q2nr",
+ state: LoanVaultState.IN_LIQUIDATION,
+ batchCount: 2,
+ liquidationHeight: 1799,
+ liquidationPenalty: 5,
+ batches: [loanBatchFoo, loanBatchBar],
+ },
+ ];
+
+ const bidHistory: VaultAuctionBatchHistory[] = [
+ {
+ id: "a40dca4568cb17bbcf93cffe25e25a65b028a843ae43c26c33472eb5f5cdd404-0-61913320d0bfd8e10ddeab6d19a68c79394d46aa3f8748d1152687ecee687b43",
+ key: "a40dca4568cb17bbcf93cffe25e25a65b028a843ae43c26c33472eb5f5cdd404-0",
+ sort: "0000132a-61913320d0bfd8e10ddeab6d19a68c79394d46aa3f8748d1152687ecee687b43",
+ address: "mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy",
+ vaultId:
+ "a40dca4568cb17bbcf93cffe25e25a65b028a843ae43c26c33472eb5f5cdd404",
+ index: 0,
+ from: "001489467aaf77a983e76d1152e62e9e54ad7e49da6c",
+ amount: "28.37210284",
+ tokenId: 11,
+ block: {
+ hash: "187b623cc714429ba0719262e70b794a6165bcf5a833e25eaf71154c8462b522",
+ height: 4906,
+ medianTime: 1641919036,
+ time: 1641919041,
+ },
+ },
+ ];
+
+ beforeEach(() => {
+ initialState = {
+ auctions: [],
+ bidHistory: [],
+ hasFetchAuctionsData: false,
+ };
+ });
+
+ it("should handle initial state", () => {
+ expect(auctions.reducer(undefined, { type: "unknown" })).toEqual({
+ auctions: [],
+ bidHistory: [],
+ hasFetchAuctionsData: false,
+ });
+ });
+
+ it("should handle fetch auctions and set has fetched auction flag", () => {
+ const action = { type: fetchAuctions.fulfilled, payload: liquidatedVaults };
+ const actual = auctions.reducer(initialState, action);
+ expect(actual.auctions).toStrictEqual(liquidatedVaults);
+ expect(actual.hasFetchAuctionsData).toStrictEqual(true);
+ });
+
+ it("should handle fetch auctions history", () => {
+ const action = { type: fetchBidHistory.fulfilled, payload: bidHistory };
+ const actual = auctions.reducer(initialState, action);
+ expect(actual.bidHistory).toStrictEqual(bidHistory);
+ });
+
+ it("should be able to return loan batches", () => {
+ const state = {
+ ...initialState,
+ auctions: liquidatedVaults,
+ };
+ const actual = getAuctionBatches(state);
+ expect(actual).toStrictEqual([
+ {
+ ...loanBatchFoo,
+ collateralTokenSymbols: ["DFI"],
+ auction: liquidatedVaults[0],
+ },
+ {
+ ...loanBatchFoo,
+ collateralTokenSymbols: ["DFI"],
+ auction: liquidatedVaults[1],
+ },
+ {
+ ...loanBatchBar,
+ collateralTokenSymbols: ["DFI"],
+ auction: liquidatedVaults[1],
+ },
+ ]);
+ });
+});
diff --git a/walletkit/walletkit-ui/store/block.ts b/walletkit/walletkit-ui/store/block.ts
new file mode 100644
index 0000000000..7191f14a73
--- /dev/null
+++ b/walletkit/walletkit-ui/store/block.ts
@@ -0,0 +1,57 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+// TODO(@thedoublejay): see https://github.com/microsoft/TypeScript/issues/47663
+import type {} from "immer";
+
+export interface BlockState {
+ count?: number;
+ masternodeCount?: number;
+ lastSync?: string;
+ connected: boolean;
+ isPolling: boolean;
+ tvl?: number;
+ lastSuccessfulSync?: string;
+}
+
+const initialState: BlockState = {
+ count: undefined,
+ masternodeCount: undefined,
+ lastSync: undefined,
+ connected: false,
+ isPolling: false,
+ tvl: undefined,
+ lastSuccessfulSync: undefined,
+};
+
+export const block = createSlice({
+ name: "block",
+ initialState,
+ reducers: {
+ updateBlockDetails: (
+ state,
+ action: PayloadAction<{
+ count: number;
+ masternodeCount: number;
+ lastSync?: string;
+ lastSuccessfulSync?: string;
+ tvl?: number;
+ }>,
+ ) => {
+ state.count = action.payload.count;
+ state.masternodeCount = action.payload.masternodeCount;
+ const firstSuccessfulSync =
+ state.lastSuccessfulSync ?? new Date().toString();
+ state.lastSuccessfulSync =
+ action.payload.lastSuccessfulSync != null
+ ? action.payload.lastSuccessfulSync
+ : firstSuccessfulSync;
+ state.lastSync = action.payload.lastSync; // updated even if its not successful (no connection)
+ state.tvl = action.payload.tvl;
+ },
+ setConnected: (state, action: PayloadAction) => {
+ state.connected = action.payload;
+ },
+ setPolling: (state, action: PayloadAction) => {
+ state.isPolling = action.payload;
+ },
+ },
+});
diff --git a/walletkit/walletkit-ui/store/block.unit.ts b/walletkit/walletkit-ui/store/block.unit.ts
new file mode 100644
index 0000000000..7077c1cb08
--- /dev/null
+++ b/walletkit/walletkit-ui/store/block.unit.ts
@@ -0,0 +1,68 @@
+import { block, BlockState } from "./block";
+
+describe("block reducer", () => {
+ let initialState: BlockState;
+ const date = new Date().toString();
+
+ beforeEach(() => {
+ initialState = {
+ count: 77,
+ masternodeCount: 10,
+ lastSuccessfulSync: date,
+ lastSync: date,
+ isPolling: false,
+ connected: false,
+ tvl: 1,
+ };
+ });
+
+ it("should handle initial state", () => {
+ expect(block.reducer(undefined, { type: "unknown" })).toEqual({
+ count: undefined,
+ masternodeCount: undefined,
+ lastSuccessfulSync: undefined,
+ connected: false,
+ isPolling: false,
+ tvl: undefined,
+ lastSync: undefined,
+ });
+ });
+
+ it("should handle updateBlock", () => {
+ const payload = {
+ count: 99,
+ masternodeCount: 0,
+ lastSuccessfulSync: date,
+ lastSync: date,
+ tvl: 1,
+ };
+ const actual = block.reducer(
+ initialState,
+ block.actions.updateBlockDetails(payload),
+ );
+ expect(actual).toStrictEqual({ ...initialState, ...payload });
+ });
+
+ it("should handle setConnected", () => {
+ const actual = block.reducer(
+ initialState,
+ block.actions.setConnected(true),
+ );
+ expect(actual).toStrictEqual({
+ ...initialState,
+ count: 77,
+ isPolling: false,
+ connected: true,
+ });
+ });
+
+ it("should handle setPolling", () => {
+ const actual = block.reducer(initialState, block.actions.setPolling(true));
+ expect(actual).toStrictEqual({
+ ...initialState,
+ count: 77,
+ isPolling: true,
+ connected: false,
+ });
+ });
+});
diff --git a/walletkit/walletkit-ui/store/futureSwap.ts b/walletkit/walletkit-ui/store/futureSwap.ts
new file mode 100644
index 0000000000..6cf6664d62
--- /dev/null
+++ b/walletkit/walletkit-ui/store/futureSwap.ts
@@ -0,0 +1,123 @@
+import {
+ FutureData,
+ GetFutureInfo,
+} from "@defichain/jellyfish-api-core/dist/category/account";
+import { WhaleRpcClient } from "@defichain/whale-api-client";
+import {
+ createAsyncThunk,
+ createSelector,
+ createSlice,
+ PayloadAction,
+} from "@reduxjs/toolkit";
+import BigNumber from "bignumber.js";
+
+import { selectLoansState } from "./loans";
+
+export interface FutureSwapData {
+ source: {
+ amount: string;
+ displaySymbol: string;
+ isLoanToken: boolean;
+ symbol: string;
+ tokenId: string;
+ };
+ destination: {
+ displaySymbol: string;
+ isLoanToken: boolean;
+ symbol: string;
+ tokenId: string;
+ };
+}
+
+export interface FutureSwapState {
+ futureSwaps: FutureData[];
+ executionBlock: number;
+}
+
+const initialState: FutureSwapState = {
+ futureSwaps: [],
+ executionBlock: 0,
+};
+
+export const fetchFutureSwaps = createAsyncThunk(
+ "wallet/fetchFutureSwaps",
+ async ({ client, address }: { client: WhaleRpcClient; address: string }) =>
+ client.account.getPendingFutureSwaps(address),
+);
+
+export const fetchExecutionBlock = createAsyncThunk(
+ "wallet/fetchNextFutureSwapBlock",
+ async ({ client }: { client: WhaleRpcClient }) =>
+ client.oracle.getFutureSwapBlock(),
+);
+
+export const futureSwaps = createSlice({
+ name: "futureSwaps",
+ initialState,
+ reducers: {},
+ extraReducers: (builder) => {
+ builder.addCase(
+ fetchFutureSwaps.fulfilled,
+ (state, action: PayloadAction) => {
+ state.futureSwaps = action.payload.values;
+ },
+ );
+ builder.addCase(
+ fetchExecutionBlock.fulfilled,
+ (state, action: PayloadAction) => {
+ state.executionBlock = action.payload;
+ },
+ );
+ },
+});
+
+export const selectFutureSwapState = (state: any): FutureSwapState =>
+ state.futureSwaps;
+
+export const hasFutureSwap = createSelector(
+ (state: FutureSwapState) => state.futureSwaps,
+ (swap): boolean => swap.length > 0,
+);
+
+export const futureSwapSelector = createSelector(
+ [selectFutureSwapState, selectLoansState],
+ (futureSwapsState, loans): FutureSwapData[] =>
+ Object.values(
+ futureSwapsState.futureSwaps.reduce(
+ (swaps: { [key: string]: FutureSwapData }, swap) => {
+ const [sourceAmount, sourceSymbol] = swap.source.split("@"); // ['123', 'DUSD']
+ const destinationSymbol = swap.destination;
+ const sourceLoanToken = loans.loanTokens.find(
+ (token) => token.token.symbol === sourceSymbol,
+ );
+ const destinationLoanToken = loans.loanTokens.find(
+ (token) => token.token.symbol === destinationSymbol,
+ );
+ const key = `${sourceSymbol}-${destinationSymbol}`;
+ swaps[key] = {
+ source: {
+ amount:
+ swaps[key] === undefined
+ ? new BigNumber(sourceAmount).toFixed(8)
+ : BigNumber.max(new BigNumber(swaps[key].source.amount), 0)
+ .plus(sourceAmount)
+ .toFixed(8),
+ displaySymbol: sourceLoanToken?.token.displaySymbol ?? "",
+ isLoanToken: sourceLoanToken?.token.displaySymbol !== "DUSD",
+ symbol: sourceLoanToken?.token.symbol ?? "",
+ tokenId: sourceLoanToken?.token.id ?? "",
+ },
+ destination: {
+ displaySymbol: destinationLoanToken?.token.displaySymbol ?? "",
+ isLoanToken: destinationLoanToken?.token.displaySymbol !== "DUSD",
+ symbol: destinationLoanToken?.token.symbol ?? "",
+ tokenId: destinationLoanToken?.token.id ?? "",
+ },
+ };
+
+ return swaps;
+ },
+ {},
+ ),
+ ),
+);
diff --git a/walletkit/walletkit-ui/store/futureSwap.unit.ts b/walletkit/walletkit-ui/store/futureSwap.unit.ts
new file mode 100644
index 0000000000..833fb06e9b
--- /dev/null
+++ b/walletkit/walletkit-ui/store/futureSwap.unit.ts
@@ -0,0 +1,52 @@
+import { GetFutureInfo } from "@defichain/jellyfish-api-core/dist/category/account";
+
+import {
+ fetchExecutionBlock,
+ fetchFutureSwaps,
+ futureSwaps,
+ FutureSwapState,
+} from "./futureSwap";
+
+describe("futureSwap reducer", () => {
+ let initialState: FutureSwapState;
+
+ beforeEach(() => {
+ initialState = {
+ futureSwaps: [],
+ executionBlock: 0,
+ };
+ });
+
+ it("should handle initial state", () => {
+ expect(futureSwaps.reducer(undefined, { type: "unknown" })).toEqual({
+ futureSwaps: [],
+ executionBlock: 0,
+ });
+ });
+
+ it("should handle fetchExecutionBlock", () => {
+ const action = { type: fetchExecutionBlock.fulfilled, payload: 10 };
+ const actual = futureSwaps.reducer(initialState, action);
+ expect(actual.executionBlock).toStrictEqual(10);
+ });
+
+ it("should handle fetchFutureSwaps", () => {
+ const getFutureInfo: GetFutureInfo = {
+ owner: "bcrt1qgnmfwckutkvekgulky92r3csyct0z064yvjax5",
+ values: [
+ {
+ source: "tf1qn0jv4xh60ryx4wyq8wvf0w30f77gqx3f3etyvt",
+ destination: "tf1qn0jv4xh60ryx4wyq8wvf0w30f77gqx3f3etyvt",
+ },
+ {
+ source: "tf1qn0jv4xh60ryx4wyq8wvf0w30f77gqx3f3etyvt",
+ destination: "tf1qn0jv4xh60ryx4wyq8wvf0w30f77gqx3f3etyvt",
+ },
+ ],
+ };
+
+ const action = { type: fetchFutureSwaps.fulfilled, payload: getFutureInfo };
+ const actual = futureSwaps.reducer(initialState, action);
+ expect(actual.futureSwaps).toStrictEqual(getFutureInfo.values);
+ });
+});
diff --git a/walletkit/walletkit-ui/store/index.ts b/walletkit/walletkit-ui/store/index.ts
new file mode 100644
index 0000000000..e6670def61
--- /dev/null
+++ b/walletkit/walletkit-ui/store/index.ts
@@ -0,0 +1,10 @@
+export * from "./auctions";
+export * from "./block";
+export * from "./futureSwap";
+export * from "./loans";
+export * from "./ocean";
+export * from "./transaction_queue";
+export * from "./types";
+export * from "./userPreferences";
+export * from "./wallet";
+export * from "./website";
diff --git a/walletkit/walletkit-ui/store/loans.ts b/walletkit/walletkit-ui/store/loans.ts
new file mode 100644
index 0000000000..718ddd9e1a
--- /dev/null
+++ b/walletkit/walletkit-ui/store/loans.ts
@@ -0,0 +1,232 @@
+import { WhaleApiClient } from "@defichain/whale-api-client";
+import {
+ CollateralToken,
+ LoanScheme,
+ LoanToken,
+ LoanVaultActive,
+ LoanVaultLiquidated,
+ LoanVaultState,
+} from "@defichain/whale-api-client/dist/api/loan";
+import { ActivePrice } from "@defichain/whale-api-client/dist/api/prices";
+import {
+ createAsyncThunk,
+ createSelector,
+ createSlice,
+ PayloadAction,
+} from "@reduxjs/toolkit";
+import BigNumber from "bignumber.js";
+
+import { useVaultStatus } from "../hooks";
+import { VaultStatus } from "./types";
+
+export type LoanVault = LoanVaultActive | LoanVaultLiquidated;
+
+export interface LoanPaymentTokenActivePrices {
+ [key: string]: ActivePrice;
+}
+
+export interface LoansState {
+ vaults: LoanVault[];
+ loanTokens: LoanToken[];
+ loanSchemes: LoanScheme[];
+ collateralTokens: CollateralToken[];
+ loanPaymentTokenActivePrices: LoanPaymentTokenActivePrices;
+ hasFetchedVaultsData: boolean;
+ hasFetchedLoansData: boolean;
+ hasFetchedLoanSchemes: boolean;
+}
+
+const initialState: LoansState = {
+ vaults: [],
+ loanTokens: [],
+ loanSchemes: [],
+ collateralTokens: [],
+ loanPaymentTokenActivePrices: {},
+ hasFetchedVaultsData: false,
+ hasFetchedLoansData: false,
+ hasFetchedLoanSchemes: false,
+};
+
+// TODO (Harsh) Manage pagination for all api
+export const fetchVaults = createAsyncThunk(
+ "wallet/fetchVaults",
+ async ({
+ size = 200,
+ address,
+ client,
+ }: {
+ size?: number;
+ address: string;
+ client: WhaleApiClient;
+ }) => client.address.listVault(address, size),
+);
+
+export const fetchLoanTokens = createAsyncThunk(
+ "wallet/fetchLoanTokens",
+ async ({ size = 200, client }: { size?: number; client: WhaleApiClient }) =>
+ client.loan.listLoanToken(size),
+);
+
+export const fetchLoanSchemes = createAsyncThunk(
+ "wallet/fetchLoanSchemes",
+ async ({ size = 50, client }: { size?: number; client: WhaleApiClient }) =>
+ client.loan.listScheme(size),
+);
+
+export const fetchCollateralTokens = createAsyncThunk(
+ "wallet/fetchCollateralTokens",
+ async ({ size = 50, client }: { size?: number; client: WhaleApiClient }) =>
+ client.loan.listCollateralToken(size),
+);
+
+export const fetchPrice = createAsyncThunk(
+ "wallet/fetchPrice",
+ async ({
+ client,
+ token,
+ currency,
+ }: {
+ token: string;
+ currency: string;
+ client: WhaleApiClient;
+ }) => {
+ const activePrices = await client.prices.getFeedActive(token, currency, 1);
+ return activePrices[0];
+ },
+);
+
+export const loans = createSlice({
+ name: "loans",
+ initialState,
+ reducers: {
+ setHasFetchedVaultsData: (state, action: PayloadAction) => {
+ state.hasFetchedVaultsData = action.payload;
+ },
+ },
+ extraReducers: (builder) => {
+ builder.addCase(
+ fetchVaults.fulfilled,
+ (state, action: PayloadAction) => {
+ state.vaults = action.payload;
+ state.hasFetchedVaultsData = true;
+ },
+ );
+ builder.addCase(
+ fetchLoanTokens.fulfilled,
+ (state, action: PayloadAction) => {
+ state.loanTokens = action.payload;
+ state.hasFetchedLoansData = true;
+ },
+ );
+ builder.addCase(
+ fetchLoanSchemes.fulfilled,
+ (state, action: PayloadAction) => {
+ state.loanSchemes = action.payload;
+ state.hasFetchedLoanSchemes = true;
+ },
+ );
+ builder.addCase(
+ fetchCollateralTokens.fulfilled,
+ (state, action: PayloadAction) => {
+ state.collateralTokens = action.payload;
+ },
+ );
+ builder.addCase(
+ fetchPrice.fulfilled,
+ (state, action: PayloadAction) => {
+ state.loanPaymentTokenActivePrices = {
+ ...state.loanPaymentTokenActivePrices,
+ ...{
+ [action.payload.key]: action.payload,
+ },
+ };
+ },
+ );
+ },
+});
+
+export const selectLoansState = (state: any): LoansState => state.loans;
+
+export const ascColRatioLoanScheme = createSelector(
+ (state: LoansState) => state.loanSchemes,
+ (schemes) =>
+ schemes
+ .map((c) => c)
+ .sort((a, b) =>
+ new BigNumber(a.minColRatio).minus(b.minColRatio).toNumber(),
+ ),
+);
+
+export const loanTokensSelector = createSelector(
+ (state: LoansState) => state.loanTokens,
+ (loanTokens) => loanTokens,
+);
+
+const selectTokenId = (state: LoansState, tokenId: string): string => tokenId;
+
+export const loanTokenByTokenId = createSelector(
+ [selectTokenId, loanTokensSelector],
+ (tokenId, loanTokens) =>
+ loanTokens.find((loanToken) => loanToken.token.id === tokenId),
+);
+
+export const loanPaymentTokenActivePrices = createSelector(
+ (state: LoansState) => state.loanPaymentTokenActivePrices,
+ (activePrices) => activePrices,
+);
+
+export const vaultsSelector = createSelector(
+ (state: LoansState) => state.vaults,
+ (vaults) => {
+ const order = {
+ [VaultStatus.NearLiquidation]: 1,
+ [VaultStatus.AtRisk]: 2,
+ [VaultStatus.Healthy]: 3,
+ [VaultStatus.Liquidated]: 4,
+ [VaultStatus.Ready]: 5,
+ [VaultStatus.Halted]: 6,
+ [VaultStatus.Empty]: 7,
+ [VaultStatus.Unknown]: 8,
+ };
+
+ return vaults
+ .map((vault) => {
+ if (vault.state === LoanVaultState.IN_LIQUIDATION) {
+ return {
+ ...vault,
+ vaultState: VaultStatus.Liquidated,
+ };
+ }
+
+ const colRatio = new BigNumber(vault.collateralRatio);
+ const minColRatio = new BigNumber(vault.loanScheme.minColRatio);
+ const totalLoanValue = new BigNumber(vault.loanValue);
+ const totalCollateralValue = new BigNumber(vault.collateralValue);
+ const vaultState = useVaultStatus(
+ vault.state,
+ colRatio,
+ minColRatio,
+ totalLoanValue,
+ totalCollateralValue,
+ );
+ return {
+ ...vault,
+ vaultState: vaultState.status,
+ };
+ })
+ .sort((a, b) => order[a.vaultState] - order[b.vaultState]);
+ },
+);
+
+//* Filter vaults that will be removed with Total Portfolio Amount
+export const activeVaultsSelector = createSelector(
+ vaultsSelector,
+ (vaults) =>
+ vaults.filter((value: LoanVault) =>
+ [
+ LoanVaultState.ACTIVE,
+ LoanVaultState.MAY_LIQUIDATE,
+ LoanVaultState.FROZEN,
+ ].includes(value.state),
+ ) as LoanVaultActive[],
+);
diff --git a/walletkit/walletkit-ui/store/loans.unit.ts b/walletkit/walletkit-ui/store/loans.unit.ts
new file mode 100644
index 0000000000..23969f7389
--- /dev/null
+++ b/walletkit/walletkit-ui/store/loans.unit.ts
@@ -0,0 +1,623 @@
+import {
+ CollateralToken,
+ LoanScheme,
+ LoanToken,
+ LoanVaultLiquidated,
+ LoanVaultState,
+} from "@defichain/whale-api-client/dist/api/loan";
+
+import {
+ ascColRatioLoanScheme,
+ fetchCollateralTokens,
+ fetchLoanSchemes,
+ fetchLoanTokens,
+ fetchVaults,
+ loans,
+ LoansState,
+ loanTokenByTokenId,
+ loanTokensSelector,
+ LoanVault,
+ vaultsSelector,
+} from "./loans";
+import { VaultStatus } from "./types";
+
+describe("loans reducer", () => {
+ let initialState: LoansState;
+ const vault: LoanVault = {
+ vaultId: "eee84f2cc56bbc51a42eaf302b76d4d1250b58b943829ee82f2fa9a46a9e4319",
+ loanScheme: {
+ id: "MIN150",
+ minColRatio: "150",
+ interestRate: "5",
+ },
+ ownerAddress: "bcrt1q39r84tmh4xp7wmg32tnza8j544lynknvy8q2nr",
+ state: LoanVaultState.ACTIVE,
+ informativeRatio: "9999.94300032",
+ collateralRatio: "10000",
+ collateralValue: "100",
+ loanValue: "1.0000057",
+ interestValue: "0.0000057",
+ collateralAmounts: [
+ {
+ id: "0",
+ amount: "1.00000000",
+ symbol: "DFI",
+ symbolKey: "DFI",
+ name: "Default Defi token",
+ displaySymbol: "DFI",
+ },
+ ],
+ loanAmounts: [
+ {
+ id: "14",
+ amount: "1.00000570",
+ symbol: "DUSD",
+ symbolKey: "DUSD",
+ name: "Decentralized USD",
+ displaySymbol: "DUSD",
+ },
+ {
+ id: "13",
+ amount: "0.00000001",
+ symbol: "TD10",
+ symbolKey: "TD10",
+ name: "Decentralized TD10",
+ displaySymbol: "dTD10",
+ },
+ ],
+ interestAmounts: [
+ {
+ id: "14",
+ amount: "0.00000570",
+ symbol: "DUSD",
+ symbolKey: "DUSD",
+ name: "Decentralized USD",
+ displaySymbol: "DUSD",
+ },
+ {
+ id: "13",
+ amount: "0.00000000",
+ symbol: "TD10",
+ symbolKey: "TD10",
+ name: "Decentralized TD10",
+ displaySymbol: "dTD10",
+ },
+ ],
+ };
+ const loanTokens: LoanToken[] = [
+ {
+ tokenId:
+ "a3a124bf5a6c37fe8d293b45bf32a16e412d06c376d0a042078279564c07ac2e",
+ token: {
+ id: "10",
+ symbol: "TD10",
+ symbolKey: "TD10",
+ name: "Decentralized TD10",
+ decimal: 8,
+ limit: "0",
+ mintable: true,
+ tradeable: true,
+ isDAT: true,
+ isLPS: false,
+ finalized: false,
+ minted: "0.0001027",
+ creation: {
+ tx: "a3a124bf5a6c37fe8d293b45bf32a16e412d06c376d0a042078279564c07ac2e",
+ height: 128,
+ },
+ destruction: {
+ tx: "0000000000000000000000000000000000000000000000000000000000000000",
+ height: -1,
+ },
+ collateralAddress: "bcrt1qyrfrpadwgw7p5eh3e9h3jmu4kwlz4prx73cqny",
+ displaySymbol: "dTD10",
+ isLoanToken: true,
+ },
+ interest: "1.5",
+ fixedIntervalPriceId: "TD10/USD",
+ },
+ {
+ tokenId:
+ "b99d32217b8fbe8872015c1a376f745a94372f061a0fb88c9b212876e4f158f5",
+ token: {
+ id: "14",
+ symbol: "DUSD",
+ symbolKey: "DUSD",
+ name: "Decentralized USD",
+ decimal: 8,
+ limit: "0",
+ mintable: true,
+ tradeable: true,
+ isDAT: true,
+ isLPS: false,
+ finalized: false,
+ minted: "20540",
+ creation: {
+ tx: "b99d32217b8fbe8872015c1a376f745a94372f061a0fb88c9b212876e4f158f5",
+ height: 128,
+ },
+ destruction: {
+ tx: "0000000000000000000000000000000000000000000000000000000000000000",
+ height: -1,
+ },
+ collateralAddress: "bcrt1qyrfrpadwgw7p5eh3e9h3jmu4kwlz4prx73cqny",
+ displaySymbol: "DUSD",
+ isLoanToken: true,
+ },
+ interest: "0",
+ fixedIntervalPriceId: "DUSD/USD",
+ },
+ {
+ tokenId:
+ "ffbaea57f155a36700c65a018aafe5e7d9984416339fb623df887f1a82b12142",
+ token: {
+ id: "11",
+ symbol: "TR50",
+ symbolKey: "TR50",
+ name: "Decentralized TR50",
+ decimal: 8,
+ limit: "0",
+ mintable: true,
+ tradeable: true,
+ isDAT: true,
+ isLPS: false,
+ finalized: false,
+ minted: "10.27",
+ creation: {
+ tx: "ffbaea57f155a36700c65a018aafe5e7d9984416339fb623df887f1a82b12142",
+ height: 128,
+ },
+ destruction: {
+ tx: "0000000000000000000000000000000000000000000000000000000000000000",
+ height: -1,
+ },
+ collateralAddress: "bcrt1qyrfrpadwgw7p5eh3e9h3jmu4kwlz4prx73cqny",
+ displaySymbol: "dTR50",
+ isLoanToken: true,
+ },
+ interest: "3",
+ fixedIntervalPriceId: "TR50/USD",
+ },
+ ];
+ const loanSchemes: LoanScheme[] = [
+ {
+ id: "MIN10000",
+ minColRatio: "1000",
+ interestRate: "0.5",
+ },
+ {
+ id: "MIN150",
+ minColRatio: "150",
+ interestRate: "5",
+ },
+ {
+ id: "MIN175",
+ minColRatio: "175",
+ interestRate: "3",
+ },
+ {
+ id: "MIN200",
+ minColRatio: "200",
+ interestRate: "2",
+ },
+ {
+ id: "MIN350",
+ minColRatio: "350",
+ interestRate: "1.5",
+ },
+ {
+ id: "MIN500",
+ minColRatio: "500",
+ interestRate: "1",
+ },
+ ];
+
+ beforeEach(() => {
+ initialState = {
+ vaults: [],
+ loanTokens: [],
+ loanSchemes: [],
+ collateralTokens: [],
+ loanPaymentTokenActivePrices: {},
+ hasFetchedLoanSchemes: false,
+ hasFetchedVaultsData: false,
+ hasFetchedLoansData: false,
+ };
+ });
+
+ it("should handle initial state", () => {
+ expect(loans.reducer(undefined, { type: "unknown" })).toEqual({
+ vaults: [],
+ loanTokens: [],
+ loanSchemes: [],
+ collateralTokens: [],
+ loanPaymentTokenActivePrices: {},
+ hasFetchedVaultsData: false,
+ hasFetchedLoansData: false,
+ hasFetchedLoanSchemes: false,
+ });
+ });
+
+ it("should handle fetch vaults", () => {
+ const action = { type: fetchVaults.fulfilled, payload: [vault] };
+ const actual = loans.reducer(initialState, action);
+ expect(actual.vaults).toStrictEqual([vault]);
+ });
+
+ it("should handle fetch loan tokens", () => {
+ const action = { type: fetchLoanTokens.fulfilled, payload: loanTokens };
+ const actual = loans.reducer(initialState, action);
+ expect(actual.loanTokens).toStrictEqual(loanTokens);
+ });
+
+ it("should handle fetch loan schemes", () => {
+ const action = { type: fetchLoanSchemes.fulfilled, payload: loanSchemes };
+ const actual = loans.reducer(initialState, action);
+ expect(actual.loanSchemes).toStrictEqual(loanSchemes);
+ });
+
+ it("should handle fetch collateral tokens", () => {
+ const collateralTokens: CollateralToken[] = [
+ {
+ tokenId:
+ "08987c2d1f3d7d5a18a331c4a173a85be34cf5d2438a3e51a2ed4ed2779a6279",
+ token: {
+ id: "8",
+ symbol: "CS25",
+ symbolKey: "CS25",
+ name: "Playground CS25",
+ decimal: 8,
+ limit: "0",
+ mintable: true,
+ tradeable: true,
+ isDAT: true,
+ isLPS: false,
+ finalized: false,
+ minted: "100000000",
+ creation: {
+ tx: "37e2279b80e68f55fe1ccf9920a084731cd08e331a5ee6f7769759263e66bdcb",
+ height: 118,
+ },
+ destruction: {
+ tx: "0000000000000000000000000000000000000000000000000000000000000000",
+ height: -1,
+ },
+ collateralAddress: "mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy",
+ displaySymbol: "dCS25",
+ isLoanToken: false,
+ },
+ factor: "1",
+ activateAfterBlock: 130,
+ fixedIntervalPriceId: "CS25/USD",
+ },
+ {
+ tokenId:
+ "0b990af4ede825e3b626ac3eaa72111babf0ee5e188e66ce503415e0d3f88031",
+ token: {
+ id: "3",
+ symbol: "USDT",
+ symbolKey: "USDT",
+ name: "Playground USDT",
+ decimal: 8,
+ limit: "0",
+ mintable: true,
+ tradeable: true,
+ isDAT: true,
+ isLPS: false,
+ finalized: false,
+ minted: "1000000000",
+ creation: {
+ tx: "3fba5bf3426acbe9e3aadc9827ec8eb646ee6a2e6b09eb41ce69bddfe054d03a",
+ height: 107,
+ },
+ destruction: {
+ tx: "0000000000000000000000000000000000000000000000000000000000000000",
+ height: -1,
+ },
+ collateralAddress: "mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy",
+ displaySymbol: "dUSDT",
+ isLoanToken: false,
+ },
+ factor: "1",
+ activateAfterBlock: 129,
+ fixedIntervalPriceId: "USDT/USD",
+ },
+ {
+ tokenId:
+ "11c000d76c6d45f069630ffb3534d69f1b0e1d75a1f97d9bb3fcfaa051116126",
+ token: {
+ id: "9",
+ symbol: "CR50",
+ symbolKey: "CR50",
+ name: "Playground CR50",
+ decimal: 8,
+ limit: "0",
+ mintable: true,
+ tradeable: true,
+ isDAT: true,
+ isLPS: false,
+ finalized: false,
+ minted: "100000000",
+ creation: {
+ tx: "2f35eb08a993b052cbb60fb27062c6ff6f88015c92566a243d0092c267a31462",
+ height: 120,
+ },
+ destruction: {
+ tx: "0000000000000000000000000000000000000000000000000000000000000000",
+ height: -1,
+ },
+ collateralAddress: "mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy",
+ displaySymbol: "dCR50",
+ isLoanToken: false,
+ },
+ factor: "1",
+ activateAfterBlock: 130,
+ fixedIntervalPriceId: "CR50/USD",
+ },
+ ];
+ const action = {
+ type: fetchCollateralTokens.fulfilled,
+ payload: collateralTokens,
+ };
+ const actual = loans.reducer(initialState, action);
+ expect(actual.collateralTokens).toStrictEqual(collateralTokens);
+ });
+
+ it("should be able to select loan schemes with ascending collateralization ratio", () => {
+ const state = {
+ ...initialState,
+ loanSchemes,
+ };
+ const actual = ascColRatioLoanScheme(state);
+ expect(actual).toStrictEqual([
+ {
+ id: "MIN150",
+ minColRatio: "150",
+ interestRate: "5",
+ },
+ {
+ id: "MIN175",
+ minColRatio: "175",
+ interestRate: "3",
+ },
+ {
+ id: "MIN200",
+ minColRatio: "200",
+ interestRate: "2",
+ },
+ {
+ id: "MIN350",
+ minColRatio: "350",
+ interestRate: "1.5",
+ },
+ {
+ id: "MIN500",
+ minColRatio: "500",
+ interestRate: "1",
+ },
+ {
+ id: "MIN10000",
+ minColRatio: "1000",
+ interestRate: "0.5",
+ },
+ ]);
+ });
+
+ it("should be able to select loans token that returns DUSD with active price", () => {
+ const state = {
+ ...initialState,
+ loanTokens,
+ };
+ const loanTokensWithDUSDActivePrice: LoanToken[] = [
+ {
+ tokenId:
+ "a3a124bf5a6c37fe8d293b45bf32a16e412d06c376d0a042078279564c07ac2e",
+ token: {
+ id: "10",
+ symbol: "TD10",
+ symbolKey: "TD10",
+ name: "Decentralized TD10",
+ decimal: 8,
+ limit: "0",
+ mintable: true,
+ tradeable: true,
+ isDAT: true,
+ isLPS: false,
+ finalized: false,
+ minted: "0.0001027",
+ creation: {
+ tx: "a3a124bf5a6c37fe8d293b45bf32a16e412d06c376d0a042078279564c07ac2e",
+ height: 128,
+ },
+ destruction: {
+ tx: "0000000000000000000000000000000000000000000000000000000000000000",
+ height: -1,
+ },
+ collateralAddress: "bcrt1qyrfrpadwgw7p5eh3e9h3jmu4kwlz4prx73cqny",
+ displaySymbol: "dTD10",
+ isLoanToken: true,
+ },
+ interest: "1.5",
+ fixedIntervalPriceId: "TD10/USD",
+ },
+ {
+ tokenId:
+ "b99d32217b8fbe8872015c1a376f745a94372f061a0fb88c9b212876e4f158f5",
+ token: {
+ id: "14",
+ symbol: "DUSD",
+ symbolKey: "DUSD",
+ name: "Decentralized USD",
+ decimal: 8,
+ limit: "0",
+ mintable: true,
+ tradeable: true,
+ isDAT: true,
+ isLPS: false,
+ finalized: false,
+ minted: "20540",
+ creation: {
+ tx: "b99d32217b8fbe8872015c1a376f745a94372f061a0fb88c9b212876e4f158f5",
+ height: 128,
+ },
+ destruction: {
+ tx: "0000000000000000000000000000000000000000000000000000000000000000",
+ height: -1,
+ },
+ collateralAddress: "bcrt1qyrfrpadwgw7p5eh3e9h3jmu4kwlz4prx73cqny",
+ displaySymbol: "DUSD",
+ isLoanToken: true,
+ },
+ interest: "0",
+ fixedIntervalPriceId: "DUSD/USD",
+ },
+ {
+ tokenId:
+ "ffbaea57f155a36700c65a018aafe5e7d9984416339fb623df887f1a82b12142",
+ token: {
+ id: "11",
+ symbol: "TR50",
+ symbolKey: "TR50",
+ name: "Decentralized TR50",
+ decimal: 8,
+ limit: "0",
+ mintable: true,
+ tradeable: true,
+ isDAT: true,
+ isLPS: false,
+ finalized: false,
+ minted: "10.27",
+ creation: {
+ tx: "ffbaea57f155a36700c65a018aafe5e7d9984416339fb623df887f1a82b12142",
+ height: 128,
+ },
+ destruction: {
+ tx: "0000000000000000000000000000000000000000000000000000000000000000",
+ height: -1,
+ },
+ collateralAddress: "bcrt1qyrfrpadwgw7p5eh3e9h3jmu4kwlz4prx73cqny",
+ displaySymbol: "dTR50",
+ isLoanToken: true,
+ },
+ interest: "3",
+ fixedIntervalPriceId: "TR50/USD",
+ },
+ ];
+ const actual = loanTokensSelector(state);
+ expect(actual).toStrictEqual(loanTokensWithDUSDActivePrice);
+ });
+
+ it("should be able to select loan token by token ID", () => {
+ const state = {
+ ...initialState,
+ loanTokens,
+ };
+ const actual = loanTokenByTokenId(state, "14");
+ expect(actual).toStrictEqual({
+ tokenId:
+ "b99d32217b8fbe8872015c1a376f745a94372f061a0fb88c9b212876e4f158f5",
+ token: {
+ id: "14",
+ symbol: "DUSD",
+ symbolKey: "DUSD",
+ name: "Decentralized USD",
+ decimal: 8,
+ limit: "0",
+ mintable: true,
+ tradeable: true,
+ isDAT: true,
+ isLPS: false,
+ finalized: false,
+ minted: "20540",
+ creation: {
+ tx: "b99d32217b8fbe8872015c1a376f745a94372f061a0fb88c9b212876e4f158f5",
+ height: 128,
+ },
+ destruction: {
+ tx: "0000000000000000000000000000000000000000000000000000000000000000",
+ height: -1,
+ },
+ collateralAddress: "bcrt1qyrfrpadwgw7p5eh3e9h3jmu4kwlz4prx73cqny",
+ displaySymbol: "DUSD",
+ isLoanToken: true,
+ },
+ interest: "0",
+ fixedIntervalPriceId: "DUSD/USD",
+ });
+ });
+
+ it("should be able to select vaults regardless of vault state", () => {
+ const liquidatedVault: LoanVaultLiquidated & { vaultState: VaultStatus } = {
+ vaultId:
+ "eee84f2cc56bbc51a42eaf302b76d4d1250b58b943829ee82f2fa9a46a9e4319",
+ loanScheme: {
+ id: "MIN150",
+ minColRatio: "150",
+ interestRate: "5",
+ },
+ ownerAddress: "bcrt1q39r84tmh4xp7wmg32tnza8j544lynknvy8q2nr",
+ state: LoanVaultState.IN_LIQUIDATION,
+ vaultState: VaultStatus.Liquidated,
+ liquidationHeight: 1,
+ liquidationPenalty: 1,
+ batchCount: 1,
+ batches: [],
+ };
+ const state = {
+ ...initialState,
+ vaults: [liquidatedVault],
+ };
+ const actual = vaultsSelector(state);
+ expect(actual).toStrictEqual([liquidatedVault]);
+ });
+
+ it("should be able to select vaults that returns DUSD loan and interest with active price", () => {
+ const state = {
+ ...initialState,
+ vaults: [vault],
+ };
+ const actual = vaultsSelector(state);
+ expect(actual).toStrictEqual([
+ {
+ ...vault,
+ loanAmounts: [
+ {
+ id: "14",
+ amount: "1.00000570",
+ symbol: "DUSD",
+ symbolKey: "DUSD",
+ name: "Decentralized USD",
+ displaySymbol: "DUSD",
+ },
+ {
+ id: "13",
+ amount: "0.00000001",
+ symbol: "TD10",
+ symbolKey: "TD10",
+ name: "Decentralized TD10",
+ displaySymbol: "dTD10",
+ },
+ ],
+ interestAmounts: [
+ {
+ id: "14",
+ amount: "0.00000570",
+ symbol: "DUSD",
+ symbolKey: "DUSD",
+ name: "Decentralized USD",
+ displaySymbol: "DUSD",
+ },
+ {
+ id: "13",
+ amount: "0.00000000",
+ symbol: "TD10",
+ symbolKey: "TD10",
+ name: "Decentralized TD10",
+ displaySymbol: "dTD10",
+ },
+ ],
+ vaultState: "HEALTHY",
+ },
+ ]);
+ });
+});
diff --git a/walletkit/walletkit-ui/store/ocean.ts b/walletkit/walletkit-ui/store/ocean.ts
new file mode 100644
index 0000000000..93235bacd5
--- /dev/null
+++ b/walletkit/walletkit-ui/store/ocean.ts
@@ -0,0 +1,76 @@
+import { CTransactionSegWit } from "@defichain/jellyfish-transaction";
+import { createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit";
+// TODO(@thedoublejay): see https://github.com/microsoft/TypeScript/issues/47663
+import type {} from "reselect";
+
+export enum TransactionStatusCode {
+ success = 200,
+ pending = 202,
+}
+
+export interface OceanTransaction {
+ broadcasted: boolean;
+ tx: CTransactionSegWit;
+ title?: string;
+ drawerMessages?: {
+ preparing?: string;
+ waiting?: string;
+ complete?: string;
+ };
+ submitButtonLabel?: string;
+ onBroadcast?: () => any;
+ onConfirmation?: () => any;
+ onError?: () => any;
+ oceanStatusCode?: TransactionStatusCode;
+}
+
+export interface OceanState {
+ transactions: OceanTransaction[];
+ height: number;
+ err?: Error;
+}
+
+const initialState: OceanState = {
+ transactions: [],
+ height: 0,
+ err: undefined,
+};
+
+export const ocean = createSlice({
+ name: "ocean",
+ initialState,
+ reducers: {
+ setHeight: (state, action: PayloadAction) => {
+ state.height = action.payload;
+ },
+ queueTransaction: (
+ state,
+ action: PayloadAction>,
+ ) => {
+ state.transactions = [
+ ...state.transactions,
+ {
+ ...action.payload,
+ broadcasted: false,
+ },
+ ];
+ },
+ setError: (state, action: PayloadAction) => {
+ state.err = action.payload;
+ },
+ popTransaction: (state) => {
+ state.transactions.shift();
+ state.transactions = [...state.transactions];
+ },
+ },
+});
+
+export const firstTransactionSelector = createSelector(
+ (state: OceanState) => state.transactions,
+ (transactions) => transactions[0],
+);
+
+export const hasOceanTXQueued = createSelector(
+ (state: OceanState) => state.transactions,
+ (transactions) => transactions.length > 0,
+);
diff --git a/walletkit/walletkit-ui/store/ocean.unit.ts b/walletkit/walletkit-ui/store/ocean.unit.ts
new file mode 100644
index 0000000000..5329265fe0
--- /dev/null
+++ b/walletkit/walletkit-ui/store/ocean.unit.ts
@@ -0,0 +1,86 @@
+import { CTransactionSegWit } from "@defichain/jellyfish-transaction";
+import { SmartBuffer } from "smart-buffer";
+
+import { ocean, OceanState, OceanTransaction } from "./ocean";
+
+describe("ocean reducer", () => {
+ let initialState: OceanState;
+
+ beforeEach(() => {
+ initialState = {
+ transactions: [],
+ height: 0,
+ err: undefined,
+ };
+ });
+
+ it("should handle initial state", () => {
+ expect(ocean.reducer(undefined, { type: "unknown" })).toEqual({
+ transactions: [],
+ err: undefined,
+ height: 0,
+ });
+ });
+
+ it("should handle queueTransaction and popTransaction", () => {
+ const v2 =
+ "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff050393700500ffffffff038260498a040000001976a9143db7aeb218455b697e94f6ff00c548e72221231d88ac7e67ce1d0000000017a914dd7730517e0e4969b4e43677ff5bee682e53420a870000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000";
+ const buffer = SmartBuffer.fromBuffer(Buffer.from(v2, "hex"));
+ const signed = new CTransactionSegWit(buffer);
+ const payload: Omit = {
+ title: "Sending",
+ tx: signed,
+ };
+ const addedTransaction = ocean.reducer(
+ initialState,
+ ocean.actions.queueTransaction(payload),
+ );
+ expect(addedTransaction).toStrictEqual({
+ transactions: [
+ {
+ ...payload,
+ broadcasted: false,
+ },
+ ],
+ err: undefined,
+ height: 0,
+ });
+ const actual = ocean.reducer(
+ addedTransaction,
+ ocean.actions.queueTransaction(payload),
+ );
+
+ const pop = ocean.reducer(actual, ocean.actions.popTransaction());
+ expect(pop).toStrictEqual({
+ transactions: [
+ {
+ ...payload,
+ broadcasted: false,
+ },
+ ],
+ err: undefined,
+ height: 0,
+ });
+ const removed = ocean.reducer(pop, ocean.actions.popTransaction());
+ expect(removed).toStrictEqual({
+ transactions: [],
+ err: undefined,
+ height: 0,
+ });
+ });
+
+ it("should handle setError", () => {
+ const err = new Error("An error has occurred");
+ const actual = ocean.reducer(initialState, ocean.actions.setError(err));
+ expect(actual).toStrictEqual({ transactions: [], err, height: 0 });
+ });
+
+ it("should setHeight", () => {
+ const actual = ocean.reducer(initialState, ocean.actions.setHeight(77));
+ expect(actual).toStrictEqual({
+ transactions: [],
+ err: undefined,
+ height: 77,
+ });
+ });
+});
diff --git a/walletkit/walletkit-ui/store/transaction_queue.ts b/walletkit/walletkit-ui/store/transaction_queue.ts
new file mode 100644
index 0000000000..34747c7e8f
--- /dev/null
+++ b/walletkit/walletkit-ui/store/transaction_queue.ts
@@ -0,0 +1,57 @@
+import { CTransactionSegWit } from "@defichain/jellyfish-transaction";
+import { WhaleWalletAccount } from "@defichain/whale-api-wallet";
+import { createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit";
+// TODO(@thedoublejay): see https://github.com/microsoft/TypeScript/issues/47663
+import type {} from "reselect";
+
+export interface DfTxSigner {
+ sign: (account: WhaleWalletAccount) => Promise;
+ title?: string;
+ description?: string;
+ drawerMessages?: {
+ preparing?: string;
+ waiting?: string;
+ complete?: string;
+ };
+ onBroadcast?: () => any;
+ onConfirmation?: () => any;
+ onError?: () => any;
+ submitButtonLabel?: string;
+}
+
+export interface TransactionQueue {
+ transactions: DfTxSigner[];
+ err?: Error;
+}
+
+const initialState: TransactionQueue = {
+ transactions: [],
+ err: undefined,
+};
+
+export const transactionQueue = createSlice({
+ name: "tx_queue",
+ initialState,
+ reducers: {
+ push: (state, action: PayloadAction) => {
+ state.transactions = [...state.transactions, action.payload];
+ state.err = undefined;
+ },
+ pop: (state) => {
+ state.transactions.shift();
+ state.transactions = [...state.transactions];
+ },
+ setError: (state, action: PayloadAction) => {
+ state.err = action.payload;
+ },
+ },
+});
+
+export const first = createSelector(
+ (state: TransactionQueue) => state.transactions,
+ (transactions) => transactions[0],
+);
+export const hasTxQueued = createSelector(
+ (state: TransactionQueue) => state.transactions,
+ (transactions) => transactions.length > 0,
+);
diff --git a/walletkit/walletkit-ui/store/transaction_queue.unit.ts b/walletkit/walletkit-ui/store/transaction_queue.unit.ts
new file mode 100644
index 0000000000..93aacaf3dd
--- /dev/null
+++ b/walletkit/walletkit-ui/store/transaction_queue.unit.ts
@@ -0,0 +1,58 @@
+import {
+ DfTxSigner,
+ first,
+ hasTxQueued,
+ TransactionQueue,
+ transactionQueue,
+} from "./transaction_queue";
+
+describe("transaction reducer", () => {
+ let initialState: TransactionQueue;
+
+ beforeEach(() => {
+ initialState = {
+ transactions: [],
+ err: undefined,
+ };
+ });
+
+ it("should handle initial state", () => {
+ expect(transactionQueue.reducer(undefined, { type: "unknown" })).toEqual({
+ transactions: [],
+ });
+ });
+
+ it("should handle push and pop", () => {
+ const payload: DfTxSigner = {
+ sign: null as any,
+ title: "Sample Transaction",
+ };
+ const actual = transactionQueue.reducer(
+ initialState,
+ transactionQueue.actions.push(payload),
+ );
+ expect(actual).toStrictEqual({ transactions: [payload], err: undefined });
+ const pop = transactionQueue.reducer(
+ initialState,
+ transactionQueue.actions.pop(),
+ );
+ expect(pop).toStrictEqual({ transactions: [], err: undefined });
+ });
+
+ it("should able to select first and check queue transaction", () => {
+ const payload: DfTxSigner = {
+ sign: null as any,
+ title: "Sample Transaction",
+ };
+ const hasQueue = hasTxQueued({
+ ...initialState,
+ transactions: [payload],
+ });
+ expect(hasQueue).toStrictEqual(true);
+ const actual = first({
+ ...initialState,
+ transactions: [payload],
+ });
+ expect(actual).toStrictEqual(payload);
+ });
+});
diff --git a/walletkit/walletkit-ui/store/types/VaultStatus.ts b/walletkit/walletkit-ui/store/types/VaultStatus.ts
new file mode 100644
index 0000000000..1fd385ea6f
--- /dev/null
+++ b/walletkit/walletkit-ui/store/types/VaultStatus.ts
@@ -0,0 +1,33 @@
+import BigNumber from "bignumber.js";
+
+export interface CollateralizationRatioProps {
+ colRatio: BigNumber;
+ minColRatio: BigNumber;
+ totalLoanAmount: BigNumber;
+ totalCollateralValue?: BigNumber;
+}
+
+export interface CollateralizationRatioStats {
+ atRiskThreshold: BigNumber;
+ liquidatedThreshold: BigNumber;
+ isInLiquidation: boolean;
+ isAtRisk: boolean;
+ isHealthy: boolean;
+ isReady: boolean;
+}
+
+export enum VaultStatus {
+ Empty = "EMPTY",
+ Ready = "READY",
+ Healthy = "HEALTHY",
+ AtRisk = "AT RISK",
+ Halted = "HALTED",
+ NearLiquidation = "NEAR LIQUIDATION",
+ Liquidated = "IN LIQUIDATION",
+ Unknown = "UNKNOWN",
+}
+
+export interface VaultHealthItem {
+ vaultStats: CollateralizationRatioStats;
+ status: VaultStatus;
+}
diff --git a/walletkit/walletkit-ui/store/types/index.ts b/walletkit/walletkit-ui/store/types/index.ts
new file mode 100644
index 0000000000..5361bb4a66
--- /dev/null
+++ b/walletkit/walletkit-ui/store/types/index.ts
@@ -0,0 +1 @@
+export * from "./VaultStatus";
diff --git a/walletkit/walletkit-ui/store/userPreferences.ts b/walletkit/walletkit-ui/store/userPreferences.ts
new file mode 100644
index 0000000000..4e58fe9850
--- /dev/null
+++ b/walletkit/walletkit-ui/store/userPreferences.ts
@@ -0,0 +1,130 @@
+/* eslint-disable */
+
+import {
+ createAsyncThunk,
+ createSelector,
+ createSlice,
+ PayloadAction,
+} from "@reduxjs/toolkit";
+import { EnvironmentNetwork } from "@waveshq/walletkit-core";
+
+export interface LabeledAddress {
+ [address: string]: LocalAddress;
+}
+
+export interface LocalAddress {
+ address: string;
+ label: string;
+ isMine: boolean;
+ isFavourite?: boolean;
+}
+
+export interface UserPreferences {
+ addresses: LabeledAddress;
+ addressBook: LabeledAddress;
+}
+
+const prepopulateField = (addresses: LabeledAddress): LocalAddress[] => {
+ const _addresses: LabeledAddress = { ...addresses };
+
+ // pre-populate address and isFavourite flag for older app version, used for UI data model only
+ for (const address in addresses) {
+ if (addresses[address].address === undefined) {
+ const _address = {
+ ...addresses[address],
+ address,
+ isFavourite: false,
+ };
+ _addresses[address] = _address;
+ }
+ }
+ return Object.values(_addresses);
+};
+
+const initialState: UserPreferences = {
+ addresses: {},
+ addressBook: {},
+};
+
+export const fetchUserPreferences = createAsyncThunk(
+ "userPreferences/fetchUserPreferences",
+ // TODO @julio replace with type
+ async (network: EnvironmentNetwork, localStorage: any) =>
+ await localStorage.getUserPreferences(network),
+);
+
+export const setUserPreferences = createAsyncThunk(
+ "userPreferences/setUserPreferences",
+ async ({
+ network,
+ preferences,
+ localStorage,
+ }: {
+ network: EnvironmentNetwork;
+ preferences: UserPreferences;
+ // TODO @julio replace with type
+ localStorage: any;
+ }) => {
+ await localStorage.setUserPreferences(network, preferences);
+ },
+);
+
+export const setAddresses = createAsyncThunk(
+ "userPreferences/setAddresses",
+ async (addresses: LabeledAddress) => addresses,
+);
+
+export const setAddressBook = createAsyncThunk(
+ "userPreferences/setAddressBook",
+ async (addressBook: LabeledAddress) => addressBook,
+);
+
+export const userPreferences = createSlice({
+ name: "userPreferences",
+ initialState,
+ reducers: {
+ addToAddressBook: (state, action: PayloadAction) => {
+ state.addressBook = {
+ ...state.addressBook,
+ ...action.payload,
+ };
+ },
+ deleteFromAddressBook: (state, action: PayloadAction) => {
+ const { [action.payload]: _, ...newAddressBook } = state.addressBook;
+ state.addressBook = newAddressBook;
+ },
+ },
+ extraReducers: (builder) => {
+ builder.addCase(
+ fetchUserPreferences.fulfilled,
+ (state, action: PayloadAction) => {
+ state = action.payload;
+ return state;
+ },
+ );
+ builder.addCase(
+ setAddresses.fulfilled,
+ (state, action: PayloadAction) => {
+ state.addresses = action.payload;
+ return state;
+ },
+ );
+ builder.addCase(
+ setAddressBook.fulfilled,
+ (state, action: PayloadAction) => {
+ state.addressBook = action.payload;
+ return state;
+ },
+ );
+ },
+});
+
+export const selectAddressBookArray = createSelector(
+ (state: UserPreferences) => state.addressBook,
+ (addressBook) => prepopulateField(addressBook),
+);
+
+export const selectLocalWalletAddressArray = createSelector(
+ (state: UserPreferences) => state.addresses,
+ (walletAddress) => prepopulateField(walletAddress),
+);
diff --git a/walletkit/walletkit-ui/store/wallet.ts b/walletkit/walletkit-ui/store/wallet.ts
new file mode 100644
index 0000000000..8f6a39d563
--- /dev/null
+++ b/walletkit/walletkit-ui/store/wallet.ts
@@ -0,0 +1,376 @@
+import { WhaleApiClient } from "@defichain/whale-api-client";
+import { AddressToken } from "@defichain/whale-api-client/dist/api/address";
+import {
+ AllSwappableTokensResult,
+ DexPrice,
+ PoolPairData,
+} from "@defichain/whale-api-client/dist/api/poolpairs";
+import { TokenData } from "@defichain/whale-api-client/dist/api/tokens";
+import {
+ createAsyncThunk,
+ createSelector,
+ createSlice,
+ PayloadAction,
+} from "@reduxjs/toolkit";
+import { getPaginatedResponse } from "@waveshq/walletkit-core";
+import BigNumber from "bignumber.js";
+
+interface AssociatedToken {
+ [key: string]: TokenData;
+}
+
+export interface SwappableTokens {
+ [key: string]: AllSwappableTokensResult;
+}
+
+export interface DexPricesProps {
+ [symbol: string]: DexPrice;
+}
+
+export enum AddressType {
+ WalletAddress,
+ Whitelisted,
+ OthersButValid,
+}
+
+export interface WalletState {
+ utxoBalance: string;
+ tokens: WalletToken[];
+ allTokens: AssociatedToken;
+ poolpairs: DexItem[];
+ dexPrices: { [symbol: string]: DexPricesProps };
+ swappableTokens: SwappableTokens;
+ hasFetchedPoolpairData: boolean;
+ hasFetchedToken: boolean;
+ hasFetchedSwappableTokens: boolean;
+}
+
+export interface WalletToken extends AddressToken {
+ avatarSymbol: string;
+ usdAmount?: BigNumber;
+}
+
+export interface DexItem {
+ type: "your" | "available";
+ data: PoolPairData;
+}
+
+const initialState: WalletState = {
+ utxoBalance: "0",
+ tokens: [],
+ allTokens: {},
+ poolpairs: [],
+ dexPrices: {},
+ swappableTokens: {},
+ hasFetchedSwappableTokens: false,
+ hasFetchedPoolpairData: false,
+ hasFetchedToken: false,
+};
+
+const tokenDFI: WalletToken = {
+ id: "0",
+ symbol: "DFI",
+ symbolKey: "DFI",
+ isDAT: true,
+ isLPS: false,
+ isLoanToken: false,
+ amount: "0",
+ name: "DeFiChain",
+ displaySymbol: "DFI (Token)",
+ avatarSymbol: "DFI (Token)",
+};
+
+const utxoDFI: WalletToken = {
+ ...tokenDFI,
+ id: "0_utxo",
+ displaySymbol: "DFI (UTXO)",
+ avatarSymbol: "DFI (UTXO)",
+};
+
+const unifiedDFI: WalletToken = {
+ ...tokenDFI,
+ id: "0_unified",
+ displaySymbol: "DFI",
+ avatarSymbol: "DFI",
+};
+
+/**
+ * Recursively get all tokens based on pagination info from ApiPagedResponse class
+ */
+const getAllTokens = async (client: WhaleApiClient): Promise => {
+ const allTokens: TokenData[] = await getPaginatedResponse(
+ (limit, next) => client.tokens.list(limit, next),
+ );
+ return allTokens.filter((token) => token.isDAT);
+};
+
+export const setTokenSymbol = (t: AddressToken): WalletToken => {
+ let { displaySymbol } = t;
+ let avatarSymbol = t.displaySymbol;
+ if (t.id === "0") {
+ t.name = "DeFiChain";
+ displaySymbol = "DFI (Token)";
+ }
+ if (t.id === "0_utxo") {
+ displaySymbol = "DFI (UTXO)";
+ }
+ if (t.isLPS) {
+ t.name = t.name.replace("Default Defi token", "DeFiChain");
+ avatarSymbol = t.symbol;
+ }
+ return {
+ ...t,
+ displaySymbol,
+ avatarSymbol,
+ };
+};
+
+const associateTokens = (tokens: TokenData[]): AssociatedToken => {
+ const result: AssociatedToken = {};
+ tokens.forEach((token) => {
+ if (token.isDAT) {
+ result[token.displaySymbol] = token;
+ }
+ });
+ return result;
+};
+
+export const fetchPoolPairs = createAsyncThunk(
+ "wallet/fetchPoolPairs",
+ async ({
+ size = 200,
+ client,
+ }: {
+ size?: number;
+ client: WhaleApiClient;
+ }): Promise => {
+ const pairs = await client.poolpairs.list(size);
+ return pairs.map((data) => ({
+ type: "available",
+ data,
+ }));
+ },
+);
+
+export const fetchDexPrice = createAsyncThunk(
+ "wallet/fetchDexPrice",
+ async ({
+ client,
+ denomination,
+ }: {
+ size?: number;
+ client: WhaleApiClient;
+ denomination: string;
+ }): Promise<{ dexPrices: DexPricesProps; denomination: string }> => {
+ const { dexPrices } = await client.poolpairs.listDexPrices(denomination);
+ return {
+ dexPrices,
+ denomination,
+ };
+ },
+);
+
+export const fetchTokens = createAsyncThunk(
+ "wallet/fetchTokens",
+ async ({
+ size = 200,
+ address,
+ client,
+ }: {
+ size?: number;
+ address: string;
+ client: WhaleApiClient;
+ }): Promise<{
+ tokens: AddressToken[];
+ allTokens: TokenData[];
+ utxoBalance: string;
+ }> => {
+ const tokens = await client.address.listToken(address, size);
+ const allTokens = await getAllTokens(client);
+ const utxoBalance = await client.address.getBalance(address);
+ return {
+ tokens,
+ allTokens,
+ utxoBalance,
+ };
+ },
+);
+
+export const fetchSwappableTokens = createAsyncThunk(
+ "wallet/swappableTokens",
+ async ({
+ client,
+ fromTokenId,
+ }: {
+ client: WhaleApiClient;
+ fromTokenId: string;
+ }): Promise =>
+ client.poolpairs.getSwappableTokens(fromTokenId),
+);
+
+export const wallet = createSlice({
+ name: "wallet",
+ initialState,
+ reducers: {
+ setHasFetchedToken: (state, action: PayloadAction) => {
+ state.hasFetchedToken = action.payload;
+ },
+ },
+ extraReducers: (builder) => {
+ builder.addCase(
+ fetchPoolPairs.fulfilled,
+ (state, action: PayloadAction) => {
+ state.hasFetchedPoolpairData = true;
+ state.poolpairs = action.payload.filter(
+ ({ data }) =>
+ !data.symbol.includes("/v1") && !data.symbol.includes("BURN2"),
+ ); // Filter out v1 pairs due to stock split
+ },
+ );
+ builder.addCase(
+ fetchDexPrice.fulfilled,
+ (
+ state,
+ action: PayloadAction<{
+ dexPrices: DexPricesProps;
+ denomination: string;
+ }>,
+ ) => {
+ state.dexPrices = {
+ ...state.dexPrices,
+ [action.payload.denomination]: action.payload.dexPrices,
+ };
+ },
+ );
+ builder.addCase(
+ fetchTokens.fulfilled,
+ (
+ state,
+ action: PayloadAction<{
+ tokens: AddressToken[];
+ allTokens: TokenData[];
+ utxoBalance: string;
+ }>,
+ ) => {
+ state.hasFetchedToken = true;
+ state.tokens = action.payload.tokens.map(setTokenSymbol);
+ state.utxoBalance = action.payload.utxoBalance;
+ state.allTokens = associateTokens(
+ action.payload.allTokens.filter(
+ (token) => !token.symbol.includes("/v1"),
+ ),
+ ); // Filter out v1 tokens due to stock split
+ },
+ );
+ builder.addCase(
+ fetchSwappableTokens.fulfilled,
+ (state, action: PayloadAction) => {
+ state.hasFetchedSwappableTokens = true;
+ state.swappableTokens = {
+ ...state.swappableTokens,
+ ...{
+ [action.payload.fromToken.id]: action.payload,
+ },
+ };
+ },
+ );
+ },
+});
+
+const rawTokensSelector = createSelector(
+ (state: WalletState) => state.tokens,
+ (tokens) => {
+ const rawTokens: WalletToken[] = [];
+ if (!tokens.some((t) => t.id === "0_utxo")) {
+ rawTokens.push(utxoDFI);
+ }
+ if (!tokens.some((t) => t.id === "0")) {
+ rawTokens.push(tokenDFI);
+ }
+ if (!tokens.some((t) => t.id === "0_unified")) {
+ rawTokens.push(unifiedDFI);
+ }
+ return [...rawTokens, ...tokens];
+ },
+);
+
+export const tokensSelector = createSelector(
+ [rawTokensSelector, (state: WalletState) => state.utxoBalance],
+ (tokens, utxoBalance) => {
+ const utxoAmount = new BigNumber(utxoBalance);
+ const tokenAmount = new BigNumber(
+ (tokens.find((t) => t.id === "0") ?? tokenDFI).amount,
+ );
+ return tokens.map((t) => {
+ if (t.id === "0_utxo") {
+ return {
+ ...t,
+ amount: utxoAmount.toFixed(8),
+ };
+ }
+ if (t.id === "0_unified") {
+ return {
+ ...t,
+ amount: utxoAmount.plus(tokenAmount).toFixed(8),
+ };
+ }
+ return t;
+ });
+ },
+);
+
+export const DFITokenSelector = createSelector(
+ tokensSelector,
+ (tokens) => tokens.find((token) => token.id === "0") ?? tokenDFI,
+);
+
+export const DFIUtxoSelector = createSelector(
+ tokensSelector,
+ (tokens) => tokens.find((token) => token.id === "0_utxo") ?? utxoDFI,
+);
+
+export const unifiedDFISelector = createSelector(
+ tokensSelector,
+ (tokens) => tokens.find((token) => token.id === "0_unified") ?? unifiedDFI,
+);
+
+const selectTokenId = (state: WalletState, tokenId: string): string => tokenId;
+
+/**
+ * Get single token by `id` from wallet store.
+ * To get DFI Token or DFI UTXO, use `DFITokenSelector` or `DFIUtxoSelector` instead
+ */
+export const tokenSelector = createSelector(
+ [tokensSelector, selectTokenId],
+ (tokens, tokenId) =>
+ tokens.find((token) => {
+ if (tokenId === "0" || tokenId === "0_utxo") {
+ return token.id === "0_unified";
+ }
+ return token.id === tokenId;
+ }),
+);
+
+/**
+ * Get single token detail by `displaySymbol` from wallet store.
+ */
+export const tokenSelectorByDisplaySymbol = createSelector(
+ [(state: WalletState) => state.allTokens, selectTokenId],
+ (allTokens, displaySymbol) => allTokens[displaySymbol],
+);
+
+/**
+ * Get dexprices by currency denomination
+ */
+export const dexPricesSelectorByDenomination = createSelector(
+ [(state: WalletState) => state.dexPrices, selectTokenId],
+ (dexPrices, denomination) => dexPrices[denomination] ?? {},
+);
+
+/**
+ * Get single poolpair by id
+ */
+export const poolPairSelector = createSelector(
+ [(state: WalletState) => state.poolpairs, selectTokenId],
+ (poolpairs, id) => poolpairs.find((pair) => pair.data.id === id),
+);
diff --git a/walletkit/walletkit-ui/store/wallet.unit.ts b/walletkit/walletkit-ui/store/wallet.unit.ts
new file mode 100644
index 0000000000..4530ba037c
--- /dev/null
+++ b/walletkit/walletkit-ui/store/wallet.unit.ts
@@ -0,0 +1,316 @@
+import { TokenData } from "@defichain/whale-api-client/dist/api/tokens";
+
+import {
+ DexItem,
+ fetchPoolPairs,
+ fetchTokens,
+ tokensSelector,
+ wallet,
+ WalletState,
+ WalletToken,
+} from "./wallet";
+
+describe("wallet reducer", () => {
+ let initialState: WalletState;
+ let tokenDFI: WalletToken;
+ let utxoDFI: WalletToken;
+ let unifiedDFI: WalletToken;
+ let detailedDFI: TokenData;
+
+ const dfi = {
+ id: "0",
+ isDAT: true,
+ isLPS: false,
+ isLoanToken: false,
+ name: "DeFiChain",
+ symbol: "DFI",
+ symbolKey: "DFI",
+ displaySymbol: "DFI (Token)",
+ avatarSymbol: "DFI (Token)",
+ };
+
+ beforeEach(() => {
+ initialState = {
+ tokens: [],
+ allTokens: {},
+ utxoBalance: "0",
+ poolpairs: [],
+ dexPrices: {},
+ swappableTokens: {},
+ hasFetchedPoolpairData: false,
+ hasFetchedToken: true,
+ hasFetchedSwappableTokens: false,
+ };
+ tokenDFI = {
+ ...dfi,
+ amount: "100000",
+ };
+ utxoDFI = {
+ ...tokenDFI,
+ amount: "0",
+ id: "0_utxo",
+ displaySymbol: "DFI (UTXO)",
+ avatarSymbol: "DFI (UTXO)",
+ };
+ unifiedDFI = {
+ ...tokenDFI,
+ amount: "0",
+ id: "0_unified",
+ displaySymbol: "DFI",
+ avatarSymbol: "DFI",
+ };
+ detailedDFI = {
+ ...dfi,
+ creation: {
+ tx: "0000000000000000000000000000000000000000000000000000000000000000",
+ height: 0,
+ },
+ decimal: 8,
+ destruction: {
+ tx: "0000000000000000000000000000000000000000000000000000000000000000",
+ height: -1,
+ },
+ finalized: true,
+ limit: "0",
+ mintable: false,
+ minted: "0",
+ tradeable: true,
+ };
+ });
+
+ it("should handle initial state", () => {
+ expect(wallet.reducer(undefined, { type: "unknown" })).toEqual({
+ utxoBalance: "0",
+ tokens: [],
+ allTokens: {},
+ poolpairs: [],
+ dexPrices: {},
+ swappableTokens: {},
+ hasFetchedPoolpairData: false,
+ hasFetchedSwappableTokens: false,
+ hasFetchedToken: false,
+ });
+ });
+
+ it("should handle setTokens and setUtxoBalance", () => {
+ const tokens: WalletToken[] = [tokenDFI, utxoDFI];
+ const allTokens = {
+ "DFI (Token)": detailedDFI,
+ };
+
+ const utxoBalance = "77";
+ const action = {
+ type: fetchTokens.fulfilled.type,
+ payload: { tokens, utxoBalance, allTokens: [detailedDFI] },
+ };
+ const actual = wallet.reducer(initialState, action);
+ expect(actual.tokens).toStrictEqual(tokens);
+ expect(actual.utxoBalance).toStrictEqual("77");
+ expect(actual.allTokens).toStrictEqual(allTokens);
+ });
+
+ it("should filter out v1 tokens", () => {
+ const allTokens: TokenData[] = [
+ {
+ id: "0",
+ symbol: "AMZN/v1",
+ symbolKey: "AMZN/v1",
+ name: "dAMZN",
+ decimal: 8,
+ limit: "0",
+ mintable: false,
+ tradeable: false,
+ isDAT: true,
+ isLPS: false,
+ isLoanToken: false,
+ finalized: true,
+ minted: "0",
+ creation: {
+ tx: "0000000000000000000000000000000000000000000000000000000000000000",
+ height: 1,
+ },
+ destruction: {
+ tx: "0000000000000000000000000000000000000000000000000000000000000000",
+ height: 0,
+ },
+ displaySymbol: "dAMZN/v1",
+ },
+ ];
+ const tokens: WalletToken[] = [tokenDFI, utxoDFI];
+ const utxoBalance = "77";
+
+ const action = {
+ type: fetchTokens.fulfilled.type,
+ payload: { tokens, utxoBalance, allTokens },
+ };
+ const actual = wallet.reducer(initialState, action);
+ expect(Object.keys(actual.allTokens).length).toStrictEqual(0);
+ });
+
+ it("should handle setPoolpairs", () => {
+ const payload: DexItem[] = [
+ {
+ type: "available",
+ data: {
+ id: "8",
+ symbol: "DFI-USDT",
+ name: "Default Defi token-Playground USDT",
+ status: true,
+ displaySymbol: "dUSDT-DFI",
+ tokenA: {
+ name: "DeFiChain",
+ id: "0",
+ reserve: "1000",
+ blockCommission: "0",
+ symbol: "DFI",
+ displaySymbol: "dDFI",
+ },
+ tokenB: {
+ name: "Tether",
+ id: "3",
+ reserve: "10000000",
+ blockCommission: "0",
+ symbol: "USDT",
+ displaySymbol: "dUSDT",
+ },
+ priceRatio: {
+ ab: "0.0001",
+ ba: "10000",
+ },
+ commission: "0",
+ totalLiquidity: {
+ token: "100000",
+ usd: "20000000",
+ },
+ tradeEnabled: true,
+ ownerAddress: "mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy",
+ rewardPct: "0.2",
+ creation: {
+ tx: "f691c8b0a5d362a013a7207228e618d832c0b99af8da99c847923f5f93136d60",
+ height: 119,
+ },
+ apr: {
+ reward: 133.7652,
+ total: 133.7652,
+ commission: 0,
+ },
+ rewardLoanPct: "0",
+ },
+ },
+ ];
+
+ const action = { type: fetchPoolPairs.fulfilled.type, payload };
+ const actual = wallet.reducer(initialState, action);
+ expect(actual.poolpairs).toStrictEqual(payload);
+ });
+
+ it("should filter out v1 Poolpairs", () => {
+ const payload: DexItem[] = [
+ {
+ type: "available",
+ data: {
+ id: "8",
+ symbol: "AMZN-DUSD/v1",
+ name: "dAMZN-Decentralized USD",
+ status: true,
+ displaySymbol: "dAMZN-dDUSD/v1",
+ tokenA: {
+ id: "0",
+ reserve: "1000",
+ blockCommission: "0",
+ symbol: "AMZN",
+ displaySymbol: "dAMZN",
+ name: "dAmazon",
+ },
+ tokenB: {
+ id: "3",
+ reserve: "10000000",
+ blockCommission: "0",
+ symbol: "DUSD/v1",
+ displaySymbol: "dDUSD/v1",
+ name: "Old DUSD",
+ },
+ priceRatio: {
+ ab: "0.0001",
+ ba: "10000",
+ },
+ commission: "0",
+ totalLiquidity: {
+ token: "100000",
+ usd: "20000000",
+ },
+ tradeEnabled: true,
+ ownerAddress: "mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy",
+ rewardPct: "0.2",
+ creation: {
+ tx: "f691c8b0a5d362a013a7207228e618d832c0b99af8da99c847923f5f93136d60",
+ height: 119,
+ },
+ apr: {
+ reward: 133.7652,
+ total: 133.7652,
+ commission: 0,
+ },
+ rewardLoanPct: "0",
+ },
+ },
+ ];
+ const action = { type: fetchPoolPairs.fulfilled.type, payload };
+ const actual = wallet.reducer(initialState, action);
+ expect(actual.poolpairs.length).toStrictEqual(0);
+ });
+
+ it("should able to select tokens with default DFIs", () => {
+ const actual = tokensSelector({
+ ...initialState,
+ utxoBalance: "77",
+ });
+ expect(actual).toStrictEqual([
+ {
+ ...utxoDFI,
+ amount: "77.00000000",
+ },
+ {
+ ...tokenDFI,
+ amount: "0",
+ },
+ {
+ ...unifiedDFI,
+ amount: "77.00000000",
+ },
+ ]);
+ });
+
+ it("should able to select tokens with existing DFI Token", () => {
+ const btc = {
+ id: "1",
+ isLPS: false,
+ name: "Bitcoin",
+ isDAT: true,
+ symbol: "BTC",
+ symbolKey: "BTC",
+ amount: "1",
+ displaySymbol: "BTC",
+ avatarSymbol: "BTC",
+ isLoanToken: false,
+ };
+ const state = {
+ ...initialState,
+ utxoBalance: "77.00000000",
+ tokens: [{ ...utxoDFI }, { ...tokenDFI }, { ...unifiedDFI }, { ...btc }],
+ };
+ const actual = tokensSelector(state);
+ expect(actual).toStrictEqual([
+ {
+ ...utxoDFI,
+ amount: "77.00000000",
+ },
+ { ...tokenDFI },
+ {
+ ...unifiedDFI,
+ amount: "100077.00000000",
+ },
+ { ...btc },
+ ]);
+ });
+});
diff --git a/walletkit/walletkit-ui/store/website.ts b/walletkit/walletkit-ui/store/website.ts
new file mode 100644
index 0000000000..0295106eed
--- /dev/null
+++ b/walletkit/walletkit-ui/store/website.ts
@@ -0,0 +1,86 @@
+import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
+import {
+ AnnouncementData,
+ DeFiChainStatus,
+ FeatureFlag,
+ PoolpairWithStabInfo,
+} from "@waveshq/walletkit-core";
+
+export const statusWebsiteSlice = createApi({
+ reducerPath: "websiteStatus",
+ baseQuery: fetchBaseQuery({
+ baseUrl: "https://api.status.jellyfishsdk.com",
+ }),
+ endpoints: (builder) => ({
+ getBlockchainStatus: builder.query({
+ query: () => ({
+ url: "/blockchain",
+ method: "GET",
+ }),
+ }),
+ // Ocean API
+ getOceanStatus: builder.query({
+ query: () => ({
+ url: "/overall",
+ method: "GET",
+ }),
+ }),
+ }),
+});
+
+export const announcementWebsiteSlice = createApi({
+ reducerPath: "website",
+ baseQuery: fetchBaseQuery({
+ baseUrl: "https://wallet.defichain.com/api/v0",
+ }),
+ endpoints: (builder) => ({
+ getAnnouncements: builder.query({
+ query: () => ({
+ url: "/announcements",
+ method: "GET",
+ headers: {
+ "Access-Control-Allow-Origin": "*",
+ mode: "no-cors",
+ },
+ }),
+ }),
+ getFeatureFlags: builder.query({
+ query: () => ({
+ url: "/settings/flags",
+ method: "GET",
+ headers: {
+ "Access-Control-Allow-Origin": "*",
+ mode: "no-cors",
+ },
+ }),
+ }),
+ getPairsWithStabilizationFee: builder.query({
+ query: (reqParams) => ({
+ url: "/wallet/pairs-with-stab-info",
+ params: reqParams,
+ method: "GET",
+ headers: {
+ "Access-Control-Allow-Origin": "*",
+ mode: "no-cors",
+ },
+ }),
+ }),
+ }),
+});
+
+const { useGetBlockchainStatusQuery, useGetOceanStatusQuery } =
+ statusWebsiteSlice;
+const {
+ useGetAnnouncementsQuery,
+ useGetFeatureFlagsQuery,
+ usePrefetch,
+ useGetPairsWithStabilizationFeeQuery,
+} = announcementWebsiteSlice;
+export {
+ useGetAnnouncementsQuery,
+ useGetBlockchainStatusQuery,
+ useGetFeatureFlagsQuery,
+ useGetOceanStatusQuery,
+ useGetPairsWithStabilizationFeeQuery,
+ usePrefetch,
+};