diff --git a/.gitignore b/.gitignore index 8ea36866..2b465ab0 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,6 @@ next-env.d.ts .idea/ -.yarn/cache \ No newline at end of file +.yarn/cache + +.turbo \ No newline at end of file diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index dd0d3fd7..3568eab1 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/package.json b/package.json index 46b1d12a..00de2f1f 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,14 @@ "build": "yarn workspace @phoenix-protocol/types run build && yarn workspace @phoenix-protocol/utils run build && yarn workspace @phoenix-protocol/contracts run build && yarn workspace @phoenix-protocol/state run build && yarn workspace @phoenix-protocol/ui run build && yarn workspace @phoenix-protocol/core run build", "storybook": "yarn workspace @phoenix-protocol/ui storybook", "dev": "yarn workspace @phoenix-protocol/core dev", + "dev:testnet": "TESTNET=true yarn dev", "build:core": "yarn workspace @phoenix-protocol/core build", "build:contracts": "yarn workspace @phoenix-protocol/contracts build", "build:state": "yarn workspace @phoenix-protocol/state build", "build:ui": "yarn workspace @phoenix-protocol/ui build", "build:utils": "yarn workspace @phoenix-protocol/utils build", - "build:types": "yarn workspace @phoenix-protocol/types build" + "build:types": "yarn workspace @phoenix-protocol/types build", + "build:strat": "yarn workspace @phoenix-protocol/strategies build" }, "repository": { "type": "git", @@ -40,10 +42,14 @@ }, "packageManager": "yarn@3.6.4", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", "@stellar/stellar-sdk": "^13.1.0", + "@storybook/react-webpack5": "^8.6.9", "@walletconnect/modal": "^2.6.2", "@walletconnect/sign-client": "^2.13.1", "@walletconnect/types": "^2.13.1", + "framer-motion": "^12.6.0", "next": "15.0.3" } } diff --git a/packages/contracts/src/fetchPho.ts b/packages/contracts/src/fetchPho.ts index dd0e68e8..4a8eaded 100644 --- a/packages/contracts/src/fetchPho.ts +++ b/packages/contracts/src/fetchPho.ts @@ -1,7 +1,12 @@ import { constants } from "@phoenix-protocol/utils"; import { PhoenixPairContract } from "."; +import { isTestnet } from "@phoenix-protocol/utils/build/constants"; export const fetchPho = async (): Promise => { + if (isTestnet) { + return 0; + } + const PairContract = new PhoenixPairContract.Client({ contractId: "CD5XNKK3B6BEF2N7ULNHHGAMOKZ7P6456BFNIHRF4WNTEDKBRWAE7IAA", networkPassphrase: constants.NETWORK_PASSPHRASE, diff --git a/packages/contracts/src/phoenix-factory/index.ts b/packages/contracts/src/phoenix-factory/index.ts index 9c4a7758..8c29ef33 100644 --- a/packages/contracts/src/phoenix-factory/index.ts +++ b/packages/contracts/src/phoenix-factory/index.ts @@ -1,4 +1,5 @@ import { Buffer } from "buffer"; +import { Address } from "@stellar/stellar-sdk"; import { AssembledTransaction, Client as ContractClient, @@ -9,10 +10,16 @@ import { } from "@stellar/stellar-sdk/contract"; import type { u32, + i32, u64, i64, + u128, i128, + u256, + i256, Option, + Typepoint, + Duration, } from "@stellar/stellar-sdk/contract"; export * from "@stellar/stellar-sdk"; export * as contract from "@stellar/stellar-sdk/contract"; @@ -24,21 +31,31 @@ if (typeof window !== "undefined") { } export const Errors = { - 1: { message: "AlreadyInitialized" }, + 100: { message: "AlreadyInitialized" }, - 2: { message: "WhiteListeEmpty" }, + 101: { message: "WhiteListeEmpty" }, - 3: { message: "NotAuthorized" }, + 102: { message: "NotAuthorized" }, - 4: { message: "LiquidityPoolNotFound" }, + 103: { message: "LiquidityPoolNotFound" }, - 5: { message: "TokenABiggerThanTokenB" }, + 104: { message: "TokenABiggerThanTokenB" }, - 6: { message: "MinStakeInvalid" }, + 105: { message: "MinStakeInvalid" }, - 7: { message: "MinRewardInvalid" }, + 106: { message: "MinRewardInvalid" }, - 8: { message: "AdminNotSet" }, + 107: { message: "AdminNotSet" }, + + 108: { message: "OverflowingOps" }, + + 109: { message: "SameAdmin" }, + + 110: { message: "NoAdminChangeInPlace" }, + + 111: { message: "AdminChangeExpired" }, + + 112: { message: "TokenDecimalsInvalid" }, }; export interface PairTupleKey { @@ -149,53 +166,22 @@ export interface LiquidityPoolInitInfo { token_init_info: TokenInitInfo; } +export interface AdminChange { + new_admin: string; + time_limit: Option; +} + +export interface AutoUnstakeInfo { + stake_amount: i128; + stake_timestamp: u64; +} + export enum PoolType { Xyk = 0, Stable = 1, } export interface Client { - /** - * Construct and simulate a initialize transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. - */ - initialize: ( - { - admin, - multihop_wasm_hash, - lp_wasm_hash, - stable_wasm_hash, - stake_wasm_hash, - token_wasm_hash, - whitelisted_accounts, - lp_token_decimals, - }: { - admin: string; - multihop_wasm_hash: Buffer; - lp_wasm_hash: Buffer; - stable_wasm_hash: Buffer; - stake_wasm_hash: Buffer; - token_wasm_hash: Buffer; - whitelisted_accounts: Array; - lp_token_decimals: u32; - }, - options?: { - /** - * The fee to pay for the transaction. Default: BASE_FEE - */ - fee?: number; - - /** - * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT - */ - timeoutInSeconds?: number; - - /** - * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true - */ - simulate?: boolean; - } - ) => Promise>; - /** * Construct and simulate a create_liquidity_pool transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ @@ -238,44 +224,25 @@ export interface Client { ) => Promise>; /** - * Construct and simulate a update_whitelisted_accounts transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. - */ - update_whitelisted_accounts: ( - { - sender, - to_add, - to_remove, - }: { sender: string; to_add: Array; to_remove: Array }, - options?: { - /** - * The fee to pay for the transaction. Default: BASE_FEE - */ - fee?: number; - - /** - * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT - */ - timeoutInSeconds?: number; - - /** - * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true - */ - simulate?: boolean; - } - ) => Promise>; - - /** - * Construct and simulate a update_wasm_hashes transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Construct and simulate a update_config transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ - update_wasm_hashes: ( + update_config: ( { + multihop_address, lp_wasm_hash, stake_wasm_hash, token_wasm_hash, + whitelisted_to_add, + whitelisted_to_remove, + lp_token_decimals, }: { + multihop_address: Option; lp_wasm_hash: Option; stake_wasm_hash: Option; token_wasm_hash: Option; + whitelisted_to_add: Option>; + whitelisted_to_remove: Option>; + lp_token_decimals: Option; }, options?: { /** @@ -293,7 +260,7 @@ export interface Client { */ simulate?: boolean; } - ) => Promise>; + ) => Promise>>; /** * Construct and simulate a query_pools transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. @@ -464,6 +431,69 @@ export interface Client { simulate?: boolean; }) => Promise>>; + /** + * Construct and simulate a propose_admin transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + propose_admin: ( + { new_admin, time_limit }: { new_admin: string; time_limit: Option }, + options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + } + ) => Promise>>; + + /** + * Construct and simulate a revoke_admin_change transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + revoke_admin_change: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>>; + + /** + * Construct and simulate a accept_admin transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + accept_admin: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>>; + /** * Construct and simulate a update transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ @@ -489,9 +519,69 @@ export interface Client { simulate?: boolean; } ) => Promise>; + + /** + * Construct and simulate a query_version transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + query_version: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>; + + /** + * Construct and simulate a add_new_key_to_storage transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + add_new_key_to_storage: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>>; } export class Client extends ContractClient { static async deploy( + /** Constructor/Initialization Args for the contract's `__constructor` method */ + { + admin, + multihop_wasm_hash, + lp_wasm_hash, + stable_wasm_hash, + stake_wasm_hash, + token_wasm_hash, + whitelisted_accounts, + lp_token_decimals, + }: { + admin: string; + multihop_wasm_hash: Buffer; + lp_wasm_hash: Buffer; + stable_wasm_hash: Buffer; + stake_wasm_hash: Buffer; + token_wasm_hash: Buffer; + whitelisted_accounts: Array; + lp_token_decimals: u32; + }, /** Options for initalizing a Client as well as for calling a method, with extras specific to deploying. */ options: MethodOptions & Omit & { @@ -503,15 +593,25 @@ export class Client extends ContractClient { format?: "hex" | "base64"; } ): Promise> { - return ContractClient.deploy(null, options); + return ContractClient.deploy( + { + admin, + multihop_wasm_hash, + lp_wasm_hash, + stable_wasm_hash, + stake_wasm_hash, + token_wasm_hash, + whitelisted_accounts, + lp_token_decimals, + }, + options + ); } constructor(public readonly options: ContractClientOptions) { super( new ContractSpec([ - "AAAAAAAAAAAAAAAKaW5pdGlhbGl6ZQAAAAAACAAAAAAAAAAFYWRtaW4AAAAAAAATAAAAAAAAABJtdWx0aWhvcF93YXNtX2hhc2gAAAAAA+4AAAAgAAAAAAAAAAxscF93YXNtX2hhc2gAAAPuAAAAIAAAAAAAAAAQc3RhYmxlX3dhc21faGFzaAAAA+4AAAAgAAAAAAAAAA9zdGFrZV93YXNtX2hhc2gAAAAD7gAAACAAAAAAAAAAD3Rva2VuX3dhc21faGFzaAAAAAPuAAAAIAAAAAAAAAAUd2hpdGVsaXN0ZWRfYWNjb3VudHMAAAPqAAAAEwAAAAAAAAARbHBfdG9rZW5fZGVjaW1hbHMAAAAAAAAEAAAAAA==", "AAAAAAAAAAAAAAAVY3JlYXRlX2xpcXVpZGl0eV9wb29sAAAAAAAACAAAAAAAAAAGc2VuZGVyAAAAAAATAAAAAAAAAAxscF9pbml0X2luZm8AAAfQAAAAFUxpcXVpZGl0eVBvb2xJbml0SW5mbwAAAAAAAAAAAAAQc2hhcmVfdG9rZW5fbmFtZQAAABAAAAAAAAAAEnNoYXJlX3Rva2VuX3N5bWJvbAAAAAAAEAAAAAAAAAAJcG9vbF90eXBlAAAAAAAH0AAAAAhQb29sVHlwZQAAAAAAAAADYW1wAAAAA+gAAAAGAAAAAAAAABRkZWZhdWx0X3NsaXBwYWdlX2JwcwAAAAcAAAAAAAAAE21heF9hbGxvd2VkX2ZlZV9icHMAAAAABwAAAAEAAAAT", - "AAAAAAAAAAAAAAAbdXBkYXRlX3doaXRlbGlzdGVkX2FjY291bnRzAAAAAAMAAAAAAAAABnNlbmRlcgAAAAAAEwAAAAAAAAAGdG9fYWRkAAAAAAPqAAAAEwAAAAAAAAAJdG9fcmVtb3ZlAAAAAAAD6gAAABMAAAAA", - "AAAAAAAAAAAAAAASdXBkYXRlX3dhc21faGFzaGVzAAAAAAADAAAAAAAAAAxscF93YXNtX2hhc2gAAAPoAAAD7gAAACAAAAAAAAAAD3N0YWtlX3dhc21faGFzaAAAAAPoAAAD7gAAACAAAAAAAAAAD3Rva2VuX3dhc21faGFzaAAAAAPoAAAD7gAAACAAAAAA", + "AAAAAAAAAAAAAAANdXBkYXRlX2NvbmZpZwAAAAAAAAcAAAAAAAAAEG11bHRpaG9wX2FkZHJlc3MAAAPoAAAAEwAAAAAAAAAMbHBfd2FzbV9oYXNoAAAD6AAAA+4AAAAgAAAAAAAAAA9zdGFrZV93YXNtX2hhc2gAAAAD6AAAA+4AAAAgAAAAAAAAAA90b2tlbl93YXNtX2hhc2gAAAAD6AAAA+4AAAAgAAAAAAAAABJ3aGl0ZWxpc3RlZF90b19hZGQAAAAAA+gAAAPqAAAAEwAAAAAAAAAVd2hpdGVsaXN0ZWRfdG9fcmVtb3ZlAAAAAAAD6AAAA+oAAAATAAAAAAAAABFscF90b2tlbl9kZWNpbWFscwAAAAAAA+gAAAAEAAAAAQAAA+kAAAfQAAAABkNvbmZpZwAAAAAH0AAAAA1Db250cmFjdEVycm9yAAAA", "AAAAAAAAAAAAAAALcXVlcnlfcG9vbHMAAAAAAAAAAAEAAAPqAAAAEw==", "AAAAAAAAAAAAAAAScXVlcnlfcG9vbF9kZXRhaWxzAAAAAAABAAAAAAAAAAxwb29sX2FkZHJlc3MAAAATAAAAAQAAB9AAAAARTGlxdWlkaXR5UG9vbEluZm8AAAA=", "AAAAAAAAAAAAAAAXcXVlcnlfYWxsX3Bvb2xzX2RldGFpbHMAAAAAAAAAAAEAAAPqAAAH0AAAABFMaXF1aWRpdHlQb29sSW5mbwAAAA==", @@ -520,8 +620,14 @@ export class Client extends ContractClient { "AAAAAAAAAAAAAAAKZ2V0X2NvbmZpZwAAAAAAAAAAAAEAAAfQAAAABkNvbmZpZwAA", "AAAAAAAAAAAAAAAUcXVlcnlfdXNlcl9wb3J0Zm9saW8AAAACAAAAAAAAAAZzZW5kZXIAAAAAABMAAAAAAAAAB3N0YWtpbmcAAAAAAQAAAAEAAAfQAAAADVVzZXJQb3J0Zm9saW8AAAA=", "AAAAAAAAAAAAAAARbWlncmF0ZV9hZG1pbl9rZXkAAAAAAAAAAAAAAQAAA+kAAAPtAAAAAAAAB9AAAAANQ29udHJhY3RFcnJvcgAAAA==", + "AAAAAAAAAAAAAAANcHJvcG9zZV9hZG1pbgAAAAAAAAIAAAAAAAAACW5ld19hZG1pbgAAAAAAABMAAAAAAAAACnRpbWVfbGltaXQAAAAAA+gAAAAGAAAAAQAAA+kAAAATAAAH0AAAAA1Db250cmFjdEVycm9yAAAA", + "AAAAAAAAAAAAAAATcmV2b2tlX2FkbWluX2NoYW5nZQAAAAAAAAAAAQAAA+kAAAPtAAAAAAAAB9AAAAANQ29udHJhY3RFcnJvcgAAAA==", + "AAAAAAAAAAAAAAAMYWNjZXB0X2FkbWluAAAAAAAAAAEAAAPpAAAAEwAAB9AAAAANQ29udHJhY3RFcnJvcgAAAA==", + "AAAAAAAAAAAAAAANX19jb25zdHJ1Y3RvcgAAAAAAAAgAAAAAAAAABWFkbWluAAAAAAAAEwAAAAAAAAASbXVsdGlob3Bfd2FzbV9oYXNoAAAAAAPuAAAAIAAAAAAAAAAMbHBfd2FzbV9oYXNoAAAD7gAAACAAAAAAAAAAEHN0YWJsZV93YXNtX2hhc2gAAAPuAAAAIAAAAAAAAAAPc3Rha2Vfd2FzbV9oYXNoAAAAA+4AAAAgAAAAAAAAAA90b2tlbl93YXNtX2hhc2gAAAAD7gAAACAAAAAAAAAAFHdoaXRlbGlzdGVkX2FjY291bnRzAAAD6gAAABMAAAAAAAAAEWxwX3Rva2VuX2RlY2ltYWxzAAAAAAAABAAAAAA=", "AAAAAAAAAAAAAAAGdXBkYXRlAAAAAAACAAAAAAAAAA1uZXdfd2FzbV9oYXNoAAAAAAAD7gAAACAAAAAAAAAAFG5ld19zdGFibGVfcG9vbF9oYXNoAAAD7gAAACAAAAAA", - "AAAABAAAAAAAAAAAAAAADUNvbnRyYWN0RXJyb3IAAAAAAAAIAAAAAAAAABJBbHJlYWR5SW5pdGlhbGl6ZWQAAAAAAAEAAAAAAAAAD1doaXRlTGlzdGVFbXB0eQAAAAACAAAAAAAAAA1Ob3RBdXRob3JpemVkAAAAAAAAAwAAAAAAAAAVTGlxdWlkaXR5UG9vbE5vdEZvdW5kAAAAAAAABAAAAAAAAAAWVG9rZW5BQmlnZ2VyVGhhblRva2VuQgAAAAAABQAAAAAAAAAPTWluU3Rha2VJbnZhbGlkAAAAAAYAAAAAAAAAEE1pblJld2FyZEludmFsaWQAAAAHAAAAAAAAAAtBZG1pbk5vdFNldAAAAAAI", + "AAAAAAAAAAAAAAANcXVlcnlfdmVyc2lvbgAAAAAAAAAAAAABAAAAEA==", + "AAAAAAAAAAAAAAAWYWRkX25ld19rZXlfdG9fc3RvcmFnZQAAAAAAAAAAAAEAAAPpAAAD7QAAAAAAAAfQAAAADUNvbnRyYWN0RXJyb3IAAAA=", + "AAAABAAAAAAAAAAAAAAADUNvbnRyYWN0RXJyb3IAAAAAAAANAAAAAAAAABJBbHJlYWR5SW5pdGlhbGl6ZWQAAAAAAGQAAAAAAAAAD1doaXRlTGlzdGVFbXB0eQAAAABlAAAAAAAAAA1Ob3RBdXRob3JpemVkAAAAAAAAZgAAAAAAAAAVTGlxdWlkaXR5UG9vbE5vdEZvdW5kAAAAAAAAZwAAAAAAAAAWVG9rZW5BQmlnZ2VyVGhhblRva2VuQgAAAAAAaAAAAAAAAAAPTWluU3Rha2VJbnZhbGlkAAAAAGkAAAAAAAAAEE1pblJld2FyZEludmFsaWQAAABqAAAAAAAAAAtBZG1pbk5vdFNldAAAAABrAAAAAAAAAA5PdmVyZmxvd2luZ09wcwAAAAAAbAAAAAAAAAAJU2FtZUFkbWluAAAAAAAAbQAAAAAAAAAUTm9BZG1pbkNoYW5nZUluUGxhY2UAAABuAAAAAAAAABJBZG1pbkNoYW5nZUV4cGlyZWQAAAAAAG8AAAAAAAAAFFRva2VuRGVjaW1hbHNJbnZhbGlkAAAAcA==", "AAAAAQAAAAAAAAAAAAAADFBhaXJUdXBsZUtleQAAAAIAAAAAAAAAB3Rva2VuX2EAAAAAEwAAAAAAAAAHdG9rZW5fYgAAAAAT", "AAAAAQAAAAAAAAAAAAAABkNvbmZpZwAAAAAABwAAAAAAAAAFYWRtaW4AAAAAAAATAAAAAAAAABFscF90b2tlbl9kZWNpbWFscwAAAAAAAAQAAAAAAAAADGxwX3dhc21faGFzaAAAA+4AAAAgAAAAAAAAABBtdWx0aWhvcF9hZGRyZXNzAAAAEwAAAAAAAAAPc3Rha2Vfd2FzbV9oYXNoAAAAA+4AAAAgAAAAAAAAAA90b2tlbl93YXNtX2hhc2gAAAAD7gAAACAAAAAAAAAAFHdoaXRlbGlzdGVkX2FjY291bnRzAAAD6gAAABM=", "AAAAAQAAAAAAAAAAAAAADVVzZXJQb3J0Zm9saW8AAAAAAAACAAAAAAAAAAxscF9wb3J0Zm9saW8AAAPqAAAH0AAAAAtMcFBvcnRmb2xpbwAAAAAAAAAAD3N0YWtlX3BvcnRmb2xpbwAAAAPqAAAH0AAAAA5TdGFrZVBvcnRmb2xpbwAA", @@ -535,16 +641,16 @@ export class Client extends ContractClient { "AAAAAQAAAAAAAAAAAAAADVRva2VuSW5pdEluZm8AAAAAAAACAAAAAAAAAAd0b2tlbl9hAAAAABMAAAAAAAAAB3Rva2VuX2IAAAAAEw==", "AAAAAQAAAAAAAAAAAAAADVN0YWtlSW5pdEluZm8AAAAAAAAEAAAAAAAAAAdtYW5hZ2VyAAAAABMAAAAAAAAADm1heF9jb21wbGV4aXR5AAAAAAAEAAAAAAAAAAhtaW5fYm9uZAAAAAsAAAAAAAAACm1pbl9yZXdhcmQAAAAAAAs=", "AAAAAQAAAAAAAAAAAAAAFUxpcXVpZGl0eVBvb2xJbml0SW5mbwAAAAAAAAkAAAAAAAAABWFkbWluAAAAAAAAEwAAAAAAAAAUZGVmYXVsdF9zbGlwcGFnZV9icHMAAAAHAAAAAAAAAA1mZWVfcmVjaXBpZW50AAAAAAAAEwAAAAAAAAAYbWF4X2FsbG93ZWRfc2xpcHBhZ2VfYnBzAAAABwAAAAAAAAAWbWF4X2FsbG93ZWRfc3ByZWFkX2JwcwAAAAAABwAAAAAAAAAQbWF4X3JlZmVycmFsX2JwcwAAAAcAAAAAAAAAD3N0YWtlX2luaXRfaW5mbwAAAAfQAAAADVN0YWtlSW5pdEluZm8AAAAAAAAAAAAADHN3YXBfZmVlX2JwcwAAAAcAAAAAAAAAD3Rva2VuX2luaXRfaW5mbwAAAAfQAAAADVRva2VuSW5pdEluZm8AAAA=", + "AAAAAQAAAAAAAAAAAAAAC0FkbWluQ2hhbmdlAAAAAAIAAAAAAAAACW5ld19hZG1pbgAAAAAAABMAAAAAAAAACnRpbWVfbGltaXQAAAAAA+gAAAAG", + "AAAAAQAAAAAAAAAAAAAAD0F1dG9VbnN0YWtlSW5mbwAAAAACAAAAAAAAAAxzdGFrZV9hbW91bnQAAAALAAAAAAAAAA9zdGFrZV90aW1lc3RhbXAAAAAABg==", "AAAAAwAAAAAAAAAAAAAACFBvb2xUeXBlAAAAAgAAAAAAAAADWHlrAAAAAAAAAAAAAAAABlN0YWJsZQAAAAAAAQ==", ]), options ); } public readonly fromJSON = { - initialize: this.txFromJSON, create_liquidity_pool: this.txFromJSON, - update_whitelisted_accounts: this.txFromJSON, - update_wasm_hashes: this.txFromJSON, + update_config: this.txFromJSON>, query_pools: this.txFromJSON>, query_pool_details: this.txFromJSON, query_all_pools_details: this.txFromJSON>, @@ -553,6 +659,11 @@ export class Client extends ContractClient { get_config: this.txFromJSON, query_user_portfolio: this.txFromJSON, migrate_admin_key: this.txFromJSON>, + propose_admin: this.txFromJSON>, + revoke_admin_change: this.txFromJSON>, + accept_admin: this.txFromJSON>, update: this.txFromJSON, + query_version: this.txFromJSON, + add_new_key_to_storage: this.txFromJSON>, }; } diff --git a/packages/contracts/src/phoenix-multihop/index.ts b/packages/contracts/src/phoenix-multihop/index.ts index 1f649860..a2aa5660 100644 --- a/packages/contracts/src/phoenix-multihop/index.ts +++ b/packages/contracts/src/phoenix-multihop/index.ts @@ -31,13 +31,19 @@ if (typeof window !== "undefined") { } export const Errors = { - 1: { message: "AlreadyInitialized" }, + 200: { message: "AlreadyInitialized" }, - 2: { message: "OperationsEmpty" }, + 201: { message: "OperationsEmpty" }, - 3: { message: "IncorrectAssetSwap" }, + 202: { message: "IncorrectAssetSwap" }, - 4: { message: "AdminNotSet" }, + 203: { message: "AdminNotSet" }, + + 204: { message: "SameAdmin" }, + + 205: { message: "NoAdminChangeInPlace" }, + + 206: { message: "AdminChangeExpired" }, }; export interface Swap { @@ -128,35 +134,22 @@ export interface LiquidityPoolInitInfo { token_init_info: TokenInitInfo; } +export interface AdminChange { + new_admin: string; + time_limit: Option; +} + +export interface AutoUnstakeInfo { + stake_amount: i128; + stake_timestamp: u64; +} + export enum PoolType { Xyk = 0, Stable = 1, } export interface Client { - /** - * Construct and simulate a initialize transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. - */ - initialize: ( - { admin, factory }: { admin: string; factory: string }, - options?: { - /** - * The fee to pay for the transaction. Default: BASE_FEE - */ - fee?: number; - - /** - * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT - */ - timeoutInSeconds?: number; - - /** - * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true - */ - simulate?: boolean; - } - ) => Promise>; - /** * Construct and simulate a swap transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ @@ -270,6 +263,69 @@ export interface Client { simulate?: boolean; }) => Promise>>; + /** + * Construct and simulate a propose_admin transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + propose_admin: ( + { new_admin, time_limit }: { new_admin: string; time_limit: Option }, + options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + } + ) => Promise>>; + + /** + * Construct and simulate a revoke_admin_change transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + revoke_admin_change: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>>; + + /** + * Construct and simulate a accept_admin transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + accept_admin: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>>; + /** * Construct and simulate a update transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ @@ -292,9 +348,51 @@ export interface Client { simulate?: boolean; } ) => Promise>; + + /** + * Construct and simulate a query_version transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + query_version: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>; + + /** + * Construct and simulate a add_new_key_to_storage transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + add_new_key_to_storage: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>>; } export class Client extends ContractClient { static async deploy( + /** Constructor/Initialization Args for the contract's `__constructor` method */ + { admin, factory }: { admin: string; factory: string }, /** Options for initalizing a Client as well as for calling a method, with extras specific to deploying. */ options: MethodOptions & Omit & { @@ -306,18 +404,23 @@ export class Client extends ContractClient { format?: "hex" | "base64"; } ): Promise> { - return ContractClient.deploy(null, options); + return ContractClient.deploy({ admin, factory }, options); } constructor(public readonly options: ContractClientOptions) { super( new ContractSpec([ - "AAAAAAAAAAAAAAAKaW5pdGlhbGl6ZQAAAAAAAgAAAAAAAAAFYWRtaW4AAAAAAAATAAAAAAAAAAdmYWN0b3J5AAAAABMAAAAA", "AAAAAAAAAAAAAAAEc3dhcAAAAAcAAAAAAAAACXJlY2lwaWVudAAAAAAAABMAAAAAAAAACm9wZXJhdGlvbnMAAAAAA+oAAAfQAAAABFN3YXAAAAAAAAAADm1heF9zcHJlYWRfYnBzAAAAAAPoAAAABwAAAAAAAAAGYW1vdW50AAAAAAALAAAAAAAAAAlwb29sX3R5cGUAAAAAAAfQAAAACFBvb2xUeXBlAAAAAAAAAAhkZWFkbGluZQAAA+gAAAAGAAAAAAAAABNtYXhfYWxsb3dlZF9mZWVfYnBzAAAAA+gAAAAHAAAAAA==", "AAAAAAAAAAAAAAANc2ltdWxhdGVfc3dhcAAAAAAAAAMAAAAAAAAACm9wZXJhdGlvbnMAAAAAA+oAAAfQAAAABFN3YXAAAAAAAAAABmFtb3VudAAAAAAACwAAAAAAAAAJcG9vbF90eXBlAAAAAAAH0AAAAAhQb29sVHlwZQAAAAEAAAfQAAAAFFNpbXVsYXRlU3dhcFJlc3BvbnNl", "AAAAAAAAAAAAAAAVc2ltdWxhdGVfcmV2ZXJzZV9zd2FwAAAAAAAAAwAAAAAAAAAKb3BlcmF0aW9ucwAAAAAD6gAAB9AAAAAEU3dhcAAAAAAAAAAGYW1vdW50AAAAAAALAAAAAAAAAAlwb29sX3R5cGUAAAAAAAfQAAAACFBvb2xUeXBlAAAAAQAAB9AAAAAbU2ltdWxhdGVSZXZlcnNlU3dhcFJlc3BvbnNlAA==", "AAAAAAAAAAAAAAARbWlncmF0ZV9hZG1pbl9rZXkAAAAAAAAAAAAAAQAAA+kAAAPtAAAAAAAAB9AAAAANQ29udHJhY3RFcnJvcgAAAA==", + "AAAAAAAAAAAAAAANcHJvcG9zZV9hZG1pbgAAAAAAAAIAAAAAAAAACW5ld19hZG1pbgAAAAAAABMAAAAAAAAACnRpbWVfbGltaXQAAAAAA+gAAAAGAAAAAQAAA+kAAAATAAAH0AAAAA1Db250cmFjdEVycm9yAAAA", + "AAAAAAAAAAAAAAATcmV2b2tlX2FkbWluX2NoYW5nZQAAAAAAAAAAAQAAA+kAAAPtAAAAAAAAB9AAAAANQ29udHJhY3RFcnJvcgAAAA==", + "AAAAAAAAAAAAAAAMYWNjZXB0X2FkbWluAAAAAAAAAAEAAAPpAAAAEwAAB9AAAAANQ29udHJhY3RFcnJvcgAAAA==", + "AAAAAAAAAAAAAAANX19jb25zdHJ1Y3RvcgAAAAAAAAIAAAAAAAAABWFkbWluAAAAAAAAEwAAAAAAAAAHZmFjdG9yeQAAAAATAAAAAA==", "AAAAAAAAAAAAAAAGdXBkYXRlAAAAAAABAAAAAAAAAA1uZXdfd2FzbV9oYXNoAAAAAAAD7gAAACAAAAAA", - "AAAABAAAAAAAAAAAAAAADUNvbnRyYWN0RXJyb3IAAAAAAAAEAAAAAAAAABJBbHJlYWR5SW5pdGlhbGl6ZWQAAAAAAAEAAAAAAAAAD09wZXJhdGlvbnNFbXB0eQAAAAACAAAAAAAAABJJbmNvcnJlY3RBc3NldFN3YXAAAAAAAAMAAAAAAAAAC0FkbWluTm90U2V0AAAAAAQ=", + "AAAAAAAAAAAAAAANcXVlcnlfdmVyc2lvbgAAAAAAAAAAAAABAAAAEA==", + "AAAAAAAAAAAAAAAWYWRkX25ld19rZXlfdG9fc3RvcmFnZQAAAAAAAAAAAAEAAAPpAAAD7QAAAAAAAAfQAAAADUNvbnRyYWN0RXJyb3IAAAA=", + "AAAABAAAAAAAAAAAAAAADUNvbnRyYWN0RXJyb3IAAAAAAAAHAAAAAAAAABJBbHJlYWR5SW5pdGlhbGl6ZWQAAAAAAMgAAAAAAAAAD09wZXJhdGlvbnNFbXB0eQAAAADJAAAAAAAAABJJbmNvcnJlY3RBc3NldFN3YXAAAAAAAMoAAAAAAAAAC0FkbWluTm90U2V0AAAAAMsAAAAAAAAACVNhbWVBZG1pbgAAAAAAAMwAAAAAAAAAFE5vQWRtaW5DaGFuZ2VJblBsYWNlAAAAzQAAAAAAAAASQWRtaW5DaGFuZ2VFeHBpcmVkAAAAAADO", "AAAAAQAAAAAAAAAAAAAABFN3YXAAAAADAAAAAAAAAAlhc2tfYXNzZXQAAAAAAAATAAAAAAAAABRhc2tfYXNzZXRfbWluX2Ftb3VudAAAA+gAAAALAAAAAAAAAAtvZmZlcl9hc3NldAAAAAAT", "AAAAAQAAAAAAAAAAAAAABFBhaXIAAAACAAAAAAAAAAd0b2tlbl9hAAAAABMAAAAAAAAAB3Rva2VuX2IAAAAAEw==", "AAAAAgAAAAAAAAAAAAAAB0RhdGFLZXkAAAAABAAAAAEAAAAAAAAAB1BhaXJLZXkAAAAAAQAAB9AAAAAEUGFpcgAAAAAAAAAAAAAACkZhY3RvcnlLZXkAAAAAAAAAAAAAAAAABUFkbWluAAAAAAAAAAAAAAAAAAALSW5pdGlhbGl6ZWQA", @@ -328,17 +431,23 @@ export class Client extends ContractClient { "AAAAAQAAAAAAAAAAAAAADVRva2VuSW5pdEluZm8AAAAAAAACAAAAAAAAAAd0b2tlbl9hAAAAABMAAAAAAAAAB3Rva2VuX2IAAAAAEw==", "AAAAAQAAAAAAAAAAAAAADVN0YWtlSW5pdEluZm8AAAAAAAAEAAAAAAAAAAdtYW5hZ2VyAAAAABMAAAAAAAAADm1heF9jb21wbGV4aXR5AAAAAAAEAAAAAAAAAAhtaW5fYm9uZAAAAAsAAAAAAAAACm1pbl9yZXdhcmQAAAAAAAs=", "AAAAAQAAAAAAAAAAAAAAFUxpcXVpZGl0eVBvb2xJbml0SW5mbwAAAAAAAAkAAAAAAAAABWFkbWluAAAAAAAAEwAAAAAAAAAUZGVmYXVsdF9zbGlwcGFnZV9icHMAAAAHAAAAAAAAAA1mZWVfcmVjaXBpZW50AAAAAAAAEwAAAAAAAAAYbWF4X2FsbG93ZWRfc2xpcHBhZ2VfYnBzAAAABwAAAAAAAAAWbWF4X2FsbG93ZWRfc3ByZWFkX2JwcwAAAAAABwAAAAAAAAAQbWF4X3JlZmVycmFsX2JwcwAAAAcAAAAAAAAAD3N0YWtlX2luaXRfaW5mbwAAAAfQAAAADVN0YWtlSW5pdEluZm8AAAAAAAAAAAAADHN3YXBfZmVlX2JwcwAAAAcAAAAAAAAAD3Rva2VuX2luaXRfaW5mbwAAAAfQAAAADVRva2VuSW5pdEluZm8AAAA=", + "AAAAAQAAAAAAAAAAAAAAC0FkbWluQ2hhbmdlAAAAAAIAAAAAAAAACW5ld19hZG1pbgAAAAAAABMAAAAAAAAACnRpbWVfbGltaXQAAAAAA+gAAAAG", + "AAAAAQAAAAAAAAAAAAAAD0F1dG9VbnN0YWtlSW5mbwAAAAACAAAAAAAAAAxzdGFrZV9hbW91bnQAAAALAAAAAAAAAA9zdGFrZV90aW1lc3RhbXAAAAAABg==", "AAAAAwAAAAAAAAAAAAAACFBvb2xUeXBlAAAAAgAAAAAAAAADWHlrAAAAAAAAAAAAAAAABlN0YWJsZQAAAAAAAQ==", ]), options ); } public readonly fromJSON = { - initialize: this.txFromJSON, swap: this.txFromJSON, simulate_swap: this.txFromJSON, simulate_reverse_swap: this.txFromJSON, migrate_admin_key: this.txFromJSON>, + propose_admin: this.txFromJSON>, + revoke_admin_change: this.txFromJSON>, + accept_admin: this.txFromJSON>, update: this.txFromJSON, + query_version: this.txFromJSON, + add_new_key_to_storage: this.txFromJSON>, }; } diff --git a/packages/contracts/src/phoenix-pair/index.ts b/packages/contracts/src/phoenix-pair/index.ts index b76358ec..048f3de9 100644 --- a/packages/contracts/src/phoenix-pair/index.ts +++ b/packages/contracts/src/phoenix-pair/index.ts @@ -31,61 +31,71 @@ if (typeof window !== "undefined") { } export const Errors = { - 1: { message: "SpreadExceedsLimit" }, + 300: { message: "SpreadExceedsLimit" }, - 2: { message: "ProvideLiquiditySlippageToleranceTooHigh" }, + 301: { message: "ProvideLiquiditySlippageToleranceTooHigh" }, - 3: { message: "ProvideLiquidityAtLeastOneTokenMustBeBiggerThenZero" }, + 302: { message: "ProvideLiquidityAtLeastOneTokenMustBeBiggerThenZero" }, - 4: { message: "WithdrawLiquidityMinimumAmountOfAOrBIsNotSatisfied" }, + 303: { message: "WithdrawLiquidityMinimumAmountOfAOrBIsNotSatisfied" }, - 5: { message: "SplitDepositBothPoolsAndDepositMustBePositive" }, + 304: { message: "SplitDepositBothPoolsAndDepositMustBePositive" }, - 6: { message: "ValidateFeeBpsTotalFeesCantBeGreaterThan100" }, + 305: { message: "ValidateFeeBpsTotalFeesCantBeGreaterThan100" }, - 7: { message: "GetDepositAmountsMinABiggerThenDesiredA" }, + 306: { message: "GetDepositAmountsMinABiggerThenDesiredA" }, - 8: { message: "GetDepositAmountsMinBBiggerThenDesiredB" }, + 307: { message: "GetDepositAmountsMinBBiggerThenDesiredB" }, - 9: { message: "GetDepositAmountsAmountABiggerThenDesiredA" }, + 308: { message: "GetDepositAmountsAmountABiggerThenDesiredA" }, - 10: { message: "GetDepositAmountsAmountALessThenMinA" }, + 309: { message: "GetDepositAmountsAmountALessThenMinA" }, - 11: { message: "GetDepositAmountsAmountBBiggerThenDesiredB" }, + 310: { message: "GetDepositAmountsAmountBBiggerThenDesiredB" }, - 12: { message: "GetDepositAmountsAmountBLessThenMinB" }, + 311: { message: "GetDepositAmountsAmountBLessThenMinB" }, - 13: { message: "TotalSharesEqualZero" }, + 312: { message: "TotalSharesEqualZero" }, - 14: { message: "DesiredAmountsBelowOrEqualZero" }, + 313: { message: "DesiredAmountsBelowOrEqualZero" }, - 15: { message: "MinAmountsBelowZero" }, + 314: { message: "MinAmountsBelowZero" }, - 16: { message: "AssetNotInPool" }, + 315: { message: "AssetNotInPool" }, - 17: { message: "AlreadyInitialized" }, + 316: { message: "AlreadyInitialized" }, - 18: { message: "TokenABiggerThanTokenB" }, + 317: { message: "TokenABiggerThanTokenB" }, - 19: { message: "InvalidBps" }, + 318: { message: "InvalidBps" }, - 20: { message: "SlippageInvalid" }, + 319: { message: "SlippageInvalid" }, - 21: { message: "SwapMinReceivedBiggerThanReturn" }, + 320: { message: "SwapMinReceivedBiggerThanReturn" }, - 22: { message: "TransactionAfterTimestampDeadline" }, + 321: { message: "TransactionAfterTimestampDeadline" }, - 23: { message: "CannotConvertU256ToI128" }, + 322: { message: "CannotConvertU256ToI128" }, - 24: { message: "UserDeclinesPoolFee" }, + 323: { message: "UserDeclinesPoolFee" }, - 25: { message: "SwapFeeBpsOverLimit" }, + 324: { message: "SwapFeeBpsOverLimit" }, - 26: { message: "NotEnoughSharesToBeMinted" }, + 325: { message: "NotEnoughSharesToBeMinted" }, - 27: { message: "NotEnoughLiquidityProvided" }, + 326: { message: "NotEnoughLiquidityProvided" }, - 28: { message: "AdminNotSet" }, + 327: { message: "AdminNotSet" }, + + 328: { message: "ContractMathError" }, + + 329: { message: "NegativeInputProvided" }, + + 330: { message: "SameAdmin" }, + + 331: { message: "NoAdminChangeInPlace" }, + + 332: { message: "AdminChangeExpired" }, }; export enum PairType { Xyk = 0, @@ -225,55 +235,22 @@ export interface LiquidityPoolInitInfo { token_init_info: TokenInitInfo; } +export interface AdminChange { + new_admin: string; + time_limit: Option; +} + +export interface AutoUnstakeInfo { + stake_amount: i128; + stake_timestamp: u64; +} + export enum PoolType { Xyk = 0, Stable = 1, } export interface Client { - /** - * Construct and simulate a initialize transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. - */ - initialize: ( - { - stake_wasm_hash, - token_wasm_hash, - lp_init_info, - factory_addr, - share_token_decimals, - share_token_name, - share_token_symbol, - default_slippage_bps, - max_allowed_fee_bps, - }: { - stake_wasm_hash: Buffer; - token_wasm_hash: Buffer; - lp_init_info: LiquidityPoolInitInfo; - factory_addr: string; - share_token_decimals: u32; - share_token_name: string; - share_token_symbol: string; - default_slippage_bps: i64; - max_allowed_fee_bps: i64; - }, - options?: { - /** - * The fee to pay for the transaction. Default: BASE_FEE - */ - fee?: number; - - /** - * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT - */ - timeoutInSeconds?: number; - - /** - * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true - */ - simulate?: boolean; - } - ) => Promise>; - /** * Construct and simulate a provide_liquidity transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ @@ -286,6 +263,7 @@ export interface Client { min_b, custom_slippage_bps, deadline, + auto_stake, }: { sender: string; desired_a: Option; @@ -294,6 +272,7 @@ export interface Client { min_b: Option; custom_slippage_bps: Option; deadline: Option; + auto_stake: boolean; }, options?: { /** @@ -362,12 +341,14 @@ export interface Client { min_a, min_b, deadline, + auto_unstake, }: { sender: string; share_amount: i128; min_a: i128; min_b: i128; deadline: Option; + auto_unstake: Option; }, options?: { /** @@ -428,10 +409,7 @@ export interface Client { * Construct and simulate a upgrade transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ upgrade: ( - { - new_wasm_hash, - new_default_slippage_bps, - }: { new_wasm_hash: Buffer; new_default_slippage_bps: i64 }, + { new_wasm_hash }: { new_wasm_hash: Buffer }, options?: { /** * The fee to pay for the transaction. Default: BASE_FEE @@ -660,10 +638,10 @@ export interface Client { }) => Promise>>; /** - * Construct and simulate a update transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Construct and simulate a propose_admin transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ - update: ( - { new_wasm_hash }: { new_wasm_hash: Buffer }, + propose_admin: ( + { new_admin, time_limit }: { new_admin: string; time_limit: Option }, options?: { /** * The fee to pay for the transaction. Default: BASE_FEE @@ -680,10 +658,110 @@ export interface Client { */ simulate?: boolean; } - ) => Promise>; + ) => Promise>>; + + /** + * Construct and simulate a revoke_admin_change transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + revoke_admin_change: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>>; + + /** + * Construct and simulate a accept_admin transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + accept_admin: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>>; + + /** + * Construct and simulate a query_version transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + query_version: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>; + + /** + * Construct and simulate a add_new_key_to_storage transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + add_new_key_to_storage: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>>; } export class Client extends ContractClient { static async deploy( + /** Constructor/Initialization Args for the contract's `__constructor` method */ + { + stake_wasm_hash, + token_wasm_hash, + lp_init_info, + factory_addr, + share_token_name, + share_token_symbol, + default_slippage_bps, + max_allowed_fee_bps, + }: { + stake_wasm_hash: Buffer; + token_wasm_hash: Buffer; + lp_init_info: LiquidityPoolInitInfo; + factory_addr: string; + share_token_name: string; + share_token_symbol: string; + default_slippage_bps: i64; + max_allowed_fee_bps: i64; + }, /** Options for initalizing a Client as well as for calling a method, with extras specific to deploying. */ options: MethodOptions & Omit & { @@ -695,17 +773,28 @@ export class Client extends ContractClient { format?: "hex" | "base64"; } ): Promise> { - return ContractClient.deploy(null, options); + return ContractClient.deploy( + { + stake_wasm_hash, + token_wasm_hash, + lp_init_info, + factory_addr, + share_token_name, + share_token_symbol, + default_slippage_bps, + max_allowed_fee_bps, + }, + options + ); } constructor(public readonly options: ContractClientOptions) { super( new ContractSpec([ - "AAAAAAAAAAAAAAAKaW5pdGlhbGl6ZQAAAAAACQAAAAAAAAAPc3Rha2Vfd2FzbV9oYXNoAAAAA+4AAAAgAAAAAAAAAA90b2tlbl93YXNtX2hhc2gAAAAD7gAAACAAAAAAAAAADGxwX2luaXRfaW5mbwAAB9AAAAAVTGlxdWlkaXR5UG9vbEluaXRJbmZvAAAAAAAAAAAAAAxmYWN0b3J5X2FkZHIAAAATAAAAAAAAABRzaGFyZV90b2tlbl9kZWNpbWFscwAAAAQAAAAAAAAAEHNoYXJlX3Rva2VuX25hbWUAAAAQAAAAAAAAABJzaGFyZV90b2tlbl9zeW1ib2wAAAAAABAAAAAAAAAAFGRlZmF1bHRfc2xpcHBhZ2VfYnBzAAAABwAAAAAAAAATbWF4X2FsbG93ZWRfZmVlX2JwcwAAAAAHAAAAAA==", - "AAAAAAAAAAAAAAARcHJvdmlkZV9saXF1aWRpdHkAAAAAAAAHAAAAAAAAAAZzZW5kZXIAAAAAABMAAAAAAAAACWRlc2lyZWRfYQAAAAAAA+gAAAALAAAAAAAAAAVtaW5fYQAAAAAAA+gAAAALAAAAAAAAAAlkZXNpcmVkX2IAAAAAAAPoAAAACwAAAAAAAAAFbWluX2IAAAAAAAPoAAAACwAAAAAAAAATY3VzdG9tX3NsaXBwYWdlX2JwcwAAAAPoAAAABwAAAAAAAAAIZGVhZGxpbmUAAAPoAAAABgAAAAA=", + "AAAAAAAAAAAAAAARcHJvdmlkZV9saXF1aWRpdHkAAAAAAAAIAAAAAAAAAAZzZW5kZXIAAAAAABMAAAAAAAAACWRlc2lyZWRfYQAAAAAAA+gAAAALAAAAAAAAAAVtaW5fYQAAAAAAA+gAAAALAAAAAAAAAAlkZXNpcmVkX2IAAAAAAAPoAAAACwAAAAAAAAAFbWluX2IAAAAAAAPoAAAACwAAAAAAAAATY3VzdG9tX3NsaXBwYWdlX2JwcwAAAAPoAAAABwAAAAAAAAAIZGVhZGxpbmUAAAPoAAAABgAAAAAAAAAKYXV0b19zdGFrZQAAAAAAAQAAAAA=", "AAAAAAAAAAAAAAAEc3dhcAAAAAcAAAAAAAAABnNlbmRlcgAAAAAAEwAAAAAAAAALb2ZmZXJfYXNzZXQAAAAAEwAAAAAAAAAMb2ZmZXJfYW1vdW50AAAACwAAAAAAAAAUYXNrX2Fzc2V0X21pbl9hbW91bnQAAAPoAAAACwAAAAAAAAAObWF4X3NwcmVhZF9icHMAAAAAA+gAAAAHAAAAAAAAAAhkZWFkbGluZQAAA+gAAAAGAAAAAAAAABNtYXhfYWxsb3dlZF9mZWVfYnBzAAAAA+gAAAAHAAAAAQAAAAs=", - "AAAAAAAAAAAAAAASd2l0aGRyYXdfbGlxdWlkaXR5AAAAAAAFAAAAAAAAAAZzZW5kZXIAAAAAABMAAAAAAAAADHNoYXJlX2Ftb3VudAAAAAsAAAAAAAAABW1pbl9hAAAAAAAACwAAAAAAAAAFbWluX2IAAAAAAAALAAAAAAAAAAhkZWFkbGluZQAAA+gAAAAGAAAAAQAAA+0AAAACAAAACwAAAAs=", + "AAAAAAAAAAAAAAASd2l0aGRyYXdfbGlxdWlkaXR5AAAAAAAGAAAAAAAAAAZzZW5kZXIAAAAAABMAAAAAAAAADHNoYXJlX2Ftb3VudAAAAAsAAAAAAAAABW1pbl9hAAAAAAAACwAAAAAAAAAFbWluX2IAAAAAAAALAAAAAAAAAAhkZWFkbGluZQAAA+gAAAAGAAAAAAAAAAxhdXRvX3Vuc3Rha2UAAAPoAAAH0AAAAA9BdXRvVW5zdGFrZUluZm8AAAAAAQAAA+0AAAACAAAACwAAAAs=", "AAAAAAAAAAAAAAANdXBkYXRlX2NvbmZpZwAAAAAAAAYAAAAAAAAACW5ld19hZG1pbgAAAAAAA+gAAAATAAAAAAAAAA10b3RhbF9mZWVfYnBzAAAAAAAD6AAAAAcAAAAAAAAADWZlZV9yZWNpcGllbnQAAAAAAAPoAAAAEwAAAAAAAAAYbWF4X2FsbG93ZWRfc2xpcHBhZ2VfYnBzAAAD6AAAAAcAAAAAAAAAFm1heF9hbGxvd2VkX3NwcmVhZF9icHMAAAAAA+gAAAAHAAAAAAAAABBtYXhfcmVmZXJyYWxfYnBzAAAD6AAAAAcAAAAA", - "AAAAAAAAAAAAAAAHdXBncmFkZQAAAAACAAAAAAAAAA1uZXdfd2FzbV9oYXNoAAAAAAAD7gAAACAAAAAAAAAAGG5ld19kZWZhdWx0X3NsaXBwYWdlX2JwcwAAAAcAAAAA", + "AAAAAAAAAAAAAAAHdXBncmFkZQAAAAABAAAAAAAAAA1uZXdfd2FzbV9oYXNoAAAAAAAD7gAAACAAAAAA", "AAAAAAAAAAAAAAAMcXVlcnlfY29uZmlnAAAAAAAAAAEAAAfQAAAABkNvbmZpZwAA", "AAAAAAAAAAAAAAAZcXVlcnlfc2hhcmVfdG9rZW5fYWRkcmVzcwAAAAAAAAAAAAABAAAAEw==", "AAAAAAAAAAAAAAAccXVlcnlfc3Rha2VfY29udHJhY3RfYWRkcmVzcwAAAAAAAAABAAAAEw==", @@ -716,8 +805,13 @@ export class Client extends ContractClient { "AAAAAAAAAAAAAAALcXVlcnlfc2hhcmUAAAAAAQAAAAAAAAAGYW1vdW50AAAAAAALAAAAAQAAA+0AAAACAAAH0AAAAAVBc3NldAAAAAAAB9AAAAAFQXNzZXQAAAA=", "AAAAAAAAAAAAAAAVcXVlcnlfdG90YWxfaXNzdWVkX2xwAAAAAAAAAAAAAAEAAAAL", "AAAAAAAAAAAAAAARbWlncmF0ZV9hZG1pbl9rZXkAAAAAAAAAAAAAAQAAA+kAAAPtAAAAAAAAB9AAAAANQ29udHJhY3RFcnJvcgAAAA==", - "AAAAAAAAAAAAAAAGdXBkYXRlAAAAAAABAAAAAAAAAA1uZXdfd2FzbV9oYXNoAAAAAAAD7gAAACAAAAAA", - "AAAABAAAAAAAAAAAAAAADUNvbnRyYWN0RXJyb3IAAAAAAAAcAAAAAAAAABJTcHJlYWRFeGNlZWRzTGltaXQAAAAAAAEAAAAAAAAAKFByb3ZpZGVMaXF1aWRpdHlTbGlwcGFnZVRvbGVyYW5jZVRvb0hpZ2gAAAACAAAAAAAAADNQcm92aWRlTGlxdWlkaXR5QXRMZWFzdE9uZVRva2VuTXVzdEJlQmlnZ2VyVGhlblplcm8AAAAAAwAAAAAAAAAyV2l0aGRyYXdMaXF1aWRpdHlNaW5pbXVtQW1vdW50T2ZBT3JCSXNOb3RTYXRpc2ZpZWQAAAAAAAQAAAAAAAAALVNwbGl0RGVwb3NpdEJvdGhQb29sc0FuZERlcG9zaXRNdXN0QmVQb3NpdGl2ZQAAAAAAAAUAAAAAAAAAK1ZhbGlkYXRlRmVlQnBzVG90YWxGZWVzQ2FudEJlR3JlYXRlclRoYW4xMDAAAAAABgAAAAAAAAAnR2V0RGVwb3NpdEFtb3VudHNNaW5BQmlnZ2VyVGhlbkRlc2lyZWRBAAAAAAcAAAAAAAAAJ0dldERlcG9zaXRBbW91bnRzTWluQkJpZ2dlclRoZW5EZXNpcmVkQgAAAAAIAAAAAAAAACpHZXREZXBvc2l0QW1vdW50c0Ftb3VudEFCaWdnZXJUaGVuRGVzaXJlZEEAAAAAAAkAAAAAAAAAJEdldERlcG9zaXRBbW91bnRzQW1vdW50QUxlc3NUaGVuTWluQQAAAAoAAAAAAAAAKkdldERlcG9zaXRBbW91bnRzQW1vdW50QkJpZ2dlclRoZW5EZXNpcmVkQgAAAAAACwAAAAAAAAAkR2V0RGVwb3NpdEFtb3VudHNBbW91bnRCTGVzc1RoZW5NaW5CAAAADAAAAAAAAAAUVG90YWxTaGFyZXNFcXVhbFplcm8AAAANAAAAAAAAAB5EZXNpcmVkQW1vdW50c0JlbG93T3JFcXVhbFplcm8AAAAAAA4AAAAAAAAAE01pbkFtb3VudHNCZWxvd1plcm8AAAAADwAAAAAAAAAOQXNzZXROb3RJblBvb2wAAAAAABAAAAAAAAAAEkFscmVhZHlJbml0aWFsaXplZAAAAAAAEQAAAAAAAAAWVG9rZW5BQmlnZ2VyVGhhblRva2VuQgAAAAAAEgAAAAAAAAAKSW52YWxpZEJwcwAAAAAAEwAAAAAAAAAPU2xpcHBhZ2VJbnZhbGlkAAAAABQAAAAAAAAAH1N3YXBNaW5SZWNlaXZlZEJpZ2dlclRoYW5SZXR1cm4AAAAAFQAAAAAAAAAhVHJhbnNhY3Rpb25BZnRlclRpbWVzdGFtcERlYWRsaW5lAAAAAAAAFgAAAAAAAAAXQ2Fubm90Q29udmVydFUyNTZUb0kxMjgAAAAAFwAAAAAAAAATVXNlckRlY2xpbmVzUG9vbEZlZQAAAAAYAAAAAAAAABNTd2FwRmVlQnBzT3ZlckxpbWl0AAAAABkAAAAAAAAAGU5vdEVub3VnaFNoYXJlc1RvQmVNaW50ZWQAAAAAAAAaAAAAAAAAABpOb3RFbm91Z2hMaXF1aWRpdHlQcm92aWRlZAAAAAAAGwAAAAAAAAALQWRtaW5Ob3RTZXQAAAAAHA==", + "AAAAAAAAAAAAAAANcHJvcG9zZV9hZG1pbgAAAAAAAAIAAAAAAAAACW5ld19hZG1pbgAAAAAAABMAAAAAAAAACnRpbWVfbGltaXQAAAAAA+gAAAAGAAAAAQAAA+kAAAATAAAH0AAAAA1Db250cmFjdEVycm9yAAAA", + "AAAAAAAAAAAAAAATcmV2b2tlX2FkbWluX2NoYW5nZQAAAAAAAAAAAQAAA+kAAAPtAAAAAAAAB9AAAAANQ29udHJhY3RFcnJvcgAAAA==", + "AAAAAAAAAAAAAAAMYWNjZXB0X2FkbWluAAAAAAAAAAEAAAPpAAAAEwAAB9AAAAANQ29udHJhY3RFcnJvcgAAAA==", + "AAAAAAAAAAAAAAANX19jb25zdHJ1Y3RvcgAAAAAAAAgAAAAAAAAAD3N0YWtlX3dhc21faGFzaAAAAAPuAAAAIAAAAAAAAAAPdG9rZW5fd2FzbV9oYXNoAAAAA+4AAAAgAAAAAAAAAAxscF9pbml0X2luZm8AAAfQAAAAFUxpcXVpZGl0eVBvb2xJbml0SW5mbwAAAAAAAAAAAAAMZmFjdG9yeV9hZGRyAAAAEwAAAAAAAAAQc2hhcmVfdG9rZW5fbmFtZQAAABAAAAAAAAAAEnNoYXJlX3Rva2VuX3N5bWJvbAAAAAAAEAAAAAAAAAAUZGVmYXVsdF9zbGlwcGFnZV9icHMAAAAHAAAAAAAAABNtYXhfYWxsb3dlZF9mZWVfYnBzAAAAAAcAAAAA", + "AAAAAAAAAAAAAAANcXVlcnlfdmVyc2lvbgAAAAAAAAAAAAABAAAAEA==", + "AAAAAAAAAAAAAAAWYWRkX25ld19rZXlfdG9fc3RvcmFnZQAAAAAAAAAAAAEAAAPpAAAD7QAAAAAAAAfQAAAADUNvbnRyYWN0RXJyb3IAAAA=", + "AAAABAAAAAAAAAAAAAAADUNvbnRyYWN0RXJyb3IAAAAAAAAhAAAAAAAAABJTcHJlYWRFeGNlZWRzTGltaXQAAAAAASwAAAAAAAAAKFByb3ZpZGVMaXF1aWRpdHlTbGlwcGFnZVRvbGVyYW5jZVRvb0hpZ2gAAAEtAAAAAAAAADNQcm92aWRlTGlxdWlkaXR5QXRMZWFzdE9uZVRva2VuTXVzdEJlQmlnZ2VyVGhlblplcm8AAAABLgAAAAAAAAAyV2l0aGRyYXdMaXF1aWRpdHlNaW5pbXVtQW1vdW50T2ZBT3JCSXNOb3RTYXRpc2ZpZWQAAAAAAS8AAAAAAAAALVNwbGl0RGVwb3NpdEJvdGhQb29sc0FuZERlcG9zaXRNdXN0QmVQb3NpdGl2ZQAAAAAAATAAAAAAAAAAK1ZhbGlkYXRlRmVlQnBzVG90YWxGZWVzQ2FudEJlR3JlYXRlclRoYW4xMDAAAAABMQAAAAAAAAAnR2V0RGVwb3NpdEFtb3VudHNNaW5BQmlnZ2VyVGhlbkRlc2lyZWRBAAAAATIAAAAAAAAAJ0dldERlcG9zaXRBbW91bnRzTWluQkJpZ2dlclRoZW5EZXNpcmVkQgAAAAEzAAAAAAAAACpHZXREZXBvc2l0QW1vdW50c0Ftb3VudEFCaWdnZXJUaGVuRGVzaXJlZEEAAAAAATQAAAAAAAAAJEdldERlcG9zaXRBbW91bnRzQW1vdW50QUxlc3NUaGVuTWluQQAAATUAAAAAAAAAKkdldERlcG9zaXRBbW91bnRzQW1vdW50QkJpZ2dlclRoZW5EZXNpcmVkQgAAAAABNgAAAAAAAAAkR2V0RGVwb3NpdEFtb3VudHNBbW91bnRCTGVzc1RoZW5NaW5CAAABNwAAAAAAAAAUVG90YWxTaGFyZXNFcXVhbFplcm8AAAE4AAAAAAAAAB5EZXNpcmVkQW1vdW50c0JlbG93T3JFcXVhbFplcm8AAAAAATkAAAAAAAAAE01pbkFtb3VudHNCZWxvd1plcm8AAAABOgAAAAAAAAAOQXNzZXROb3RJblBvb2wAAAAAATsAAAAAAAAAEkFscmVhZHlJbml0aWFsaXplZAAAAAABPAAAAAAAAAAWVG9rZW5BQmlnZ2VyVGhhblRva2VuQgAAAAABPQAAAAAAAAAKSW52YWxpZEJwcwAAAAABPgAAAAAAAAAPU2xpcHBhZ2VJbnZhbGlkAAAAAT8AAAAAAAAAH1N3YXBNaW5SZWNlaXZlZEJpZ2dlclRoYW5SZXR1cm4AAAABQAAAAAAAAAAhVHJhbnNhY3Rpb25BZnRlclRpbWVzdGFtcERlYWRsaW5lAAAAAAABQQAAAAAAAAAXQ2Fubm90Q29udmVydFUyNTZUb0kxMjgAAAABQgAAAAAAAAATVXNlckRlY2xpbmVzUG9vbEZlZQAAAAFDAAAAAAAAABNTd2FwRmVlQnBzT3ZlckxpbWl0AAAAAUQAAAAAAAAAGU5vdEVub3VnaFNoYXJlc1RvQmVNaW50ZWQAAAAAAAFFAAAAAAAAABpOb3RFbm91Z2hMaXF1aWRpdHlQcm92aWRlZAAAAAABRgAAAAAAAAALQWRtaW5Ob3RTZXQAAAABRwAAAAAAAAARQ29udHJhY3RNYXRoRXJyb3IAAAAAAAFIAAAAAAAAABVOZWdhdGl2ZUlucHV0UHJvdmlkZWQAAAAAAAFJAAAAAAAAAAlTYW1lQWRtaW4AAAAAAAFKAAAAAAAAABROb0FkbWluQ2hhbmdlSW5QbGFjZQAAAUsAAAAAAAAAEkFkbWluQ2hhbmdlRXhwaXJlZAAAAAABTA==", "AAAAAwAAAAAAAAAAAAAACFBhaXJUeXBlAAAAAQAAAAAAAAADWHlrAAAAAAA=", "AAAAAQAAAAAAAAAAAAAABkNvbmZpZwAAAAAACgAAAAAAAAANZmVlX3JlY2lwaWVudAAAAAAAABMAAABUVGhlIG1heGltdW0gYW1vdW50IG9mIHNsaXBwYWdlIChpbiBicHMpIHRoYXQgaXMgdG9sZXJhdGVkIGR1cmluZyBwcm92aWRpbmcgbGlxdWlkaXR5AAAAGG1heF9hbGxvd2VkX3NsaXBwYWdlX2JwcwAAAAcAAABDVGhlIG1heGltdW0gYW1vdW50IG9mIHNwcmVhZCAoaW4gYnBzKSB0aGF0IGlzIHRvbGVyYXRlZCBkdXJpbmcgc3dhcAAAAAAWbWF4X2FsbG93ZWRfc3ByZWFkX2JwcwAAAAAABwAAADhUaGUgbWF4aW11bSBhbGxvd2VkIHBlcmNlbnRhZ2UgKGluIGJwcykgZm9yIHJlZmVycmFsIGZlZQAAABBtYXhfcmVmZXJyYWxfYnBzAAAABwAAAAAAAAAJcG9vbF90eXBlAAAAAAAH0AAAAAhQYWlyVHlwZQAAAAAAAAALc2hhcmVfdG9rZW4AAAAAEwAAAAAAAAAOc3Rha2VfY29udHJhY3QAAAAAABMAAAAAAAAAB3Rva2VuX2EAAAAAEwAAAAAAAAAHdG9rZW5fYgAAAAATAAAAZFRoZSB0b3RhbCBmZWVzIChpbiBicHMpIGNoYXJnZWQgYnkgYSBwb29sIG9mIHRoaXMgdHlwZS4KSW4gcmVsYXRpb24gdG8gdGhlIHJldHVybmVkIGFtb3VudCBvZiB0b2tlbnMAAAANdG90YWxfZmVlX2JwcwAAAAAAAAc=", "AAAAAQAAAAAAAAAAAAAABUFzc2V0AAAAAAAAAgAAABRBZGRyZXNzIG9mIHRoZSBhc3NldAAAAAdhZGRyZXNzAAAAABMAAAAsVGhlIHRvdGFsIGFtb3VudCBvZiB0aG9zZSB0b2tlbnMgaW4gdGhlIHBvb2wAAAAGYW1vdW50AAAAAAAL", @@ -730,13 +824,14 @@ export class Client extends ContractClient { "AAAAAQAAAAAAAAAAAAAADVRva2VuSW5pdEluZm8AAAAAAAACAAAAAAAAAAd0b2tlbl9hAAAAABMAAAAAAAAAB3Rva2VuX2IAAAAAEw==", "AAAAAQAAAAAAAAAAAAAADVN0YWtlSW5pdEluZm8AAAAAAAAEAAAAAAAAAAdtYW5hZ2VyAAAAABMAAAAAAAAADm1heF9jb21wbGV4aXR5AAAAAAAEAAAAAAAAAAhtaW5fYm9uZAAAAAsAAAAAAAAACm1pbl9yZXdhcmQAAAAAAAs=", "AAAAAQAAAAAAAAAAAAAAFUxpcXVpZGl0eVBvb2xJbml0SW5mbwAAAAAAAAkAAAAAAAAABWFkbWluAAAAAAAAEwAAAAAAAAAUZGVmYXVsdF9zbGlwcGFnZV9icHMAAAAHAAAAAAAAAA1mZWVfcmVjaXBpZW50AAAAAAAAEwAAAAAAAAAYbWF4X2FsbG93ZWRfc2xpcHBhZ2VfYnBzAAAABwAAAAAAAAAWbWF4X2FsbG93ZWRfc3ByZWFkX2JwcwAAAAAABwAAAAAAAAAQbWF4X3JlZmVycmFsX2JwcwAAAAcAAAAAAAAAD3N0YWtlX2luaXRfaW5mbwAAAAfQAAAADVN0YWtlSW5pdEluZm8AAAAAAAAAAAAADHN3YXBfZmVlX2JwcwAAAAcAAAAAAAAAD3Rva2VuX2luaXRfaW5mbwAAAAfQAAAADVRva2VuSW5pdEluZm8AAAA=", + "AAAAAQAAAAAAAAAAAAAAC0FkbWluQ2hhbmdlAAAAAAIAAAAAAAAACW5ld19hZG1pbgAAAAAAABMAAAAAAAAACnRpbWVfbGltaXQAAAAAA+gAAAAG", + "AAAAAQAAAAAAAAAAAAAAD0F1dG9VbnN0YWtlSW5mbwAAAAACAAAAAAAAAAxzdGFrZV9hbW91bnQAAAALAAAAAAAAAA9zdGFrZV90aW1lc3RhbXAAAAAABg==", "AAAAAwAAAAAAAAAAAAAACFBvb2xUeXBlAAAAAgAAAAAAAAADWHlrAAAAAAAAAAAAAAAABlN0YWJsZQAAAAAAAQ==", ]), options ); } public readonly fromJSON = { - initialize: this.txFromJSON, provide_liquidity: this.txFromJSON, swap: this.txFromJSON, withdraw_liquidity: this.txFromJSON, @@ -752,6 +847,10 @@ export class Client extends ContractClient { query_share: this.txFromJSON, query_total_issued_lp: this.txFromJSON, migrate_admin_key: this.txFromJSON>, - update: this.txFromJSON, + propose_admin: this.txFromJSON>, + revoke_admin_change: this.txFromJSON>, + accept_admin: this.txFromJSON>, + query_version: this.txFromJSON, + add_new_key_to_storage: this.txFromJSON>, }; } diff --git a/packages/contracts/src/phoenix-stake/index.ts b/packages/contracts/src/phoenix-stake/index.ts index fd0036ec..2570914a 100644 --- a/packages/contracts/src/phoenix-stake/index.ts +++ b/packages/contracts/src/phoenix-stake/index.ts @@ -30,38 +30,98 @@ if (typeof window !== "undefined") { window.Buffer = window.Buffer || Buffer; } +export interface WithdrawAdjustmentKey { + asset: string; + user: string; +} + export type DistributionDataKey = | { tag: "RewardHistory"; values: readonly [string] } - | { tag: "TotalStakedHistory"; values: void }; + | { tag: "TotalStakedHistory"; values: void } + | { tag: "Curve"; values: readonly [string] } + | { tag: "Distribution"; values: readonly [string] } + | { tag: "WithdrawAdjustment"; values: readonly [WithdrawAdjustmentKey] }; + +export interface Distribution { + /** + * Bonus per staking day + */ + bonus_per_day_bps: u64; + /** + * Total rewards distributed by this contract. + */ + distributed_total: u128; + /** + * Max bonus for staking after 60 days + */ + max_bonus_bps: u64; + /** + * Shares which were not fully distributed on previous distributions, and should be redistributed + */ + shares_leftover: u64; + /** + * How many shares is single point worth + */ + shares_per_point: u128; + /** + * Total rewards not yet withdrawn. + */ + withdrawable_total: u128; +} + +export interface WithdrawAdjustment { + /** + * Represents a correction to the reward points for the user. This can be positive or negative. + * A positive value indicates that the user should receive additional points (e.g., from a bonus or an error correction), + * while a negative value signifies a reduction (e.g., due to a penalty or an adjustment for past over-allocations). + */ + shares_correction: i128; + /** + * Represents the total amount of rewards that the user has withdrawn so far. + * This value ensures that a user doesn't withdraw more than they are owed and is used to + * calculate the net rewards a user can withdraw at any given time. + */ + withdrawn_rewards: u128; +} export const Errors = { - 1: { message: "AlreadyInitialized" }, + 500: { message: "AlreadyInitialized" }, - 2: { message: "InvalidMinBond" }, + 501: { message: "InvalidMinBond" }, - 3: { message: "InvalidMinReward" }, + 502: { message: "InvalidMinReward" }, - 4: { message: "InvalidBond" }, + 503: { message: "InvalidBond" }, - 5: { message: "Unauthorized" }, + 504: { message: "Unauthorized" }, - 6: { message: "MinRewardNotEnough" }, + 505: { message: "MinRewardNotEnough" }, - 7: { message: "RewardsInvalid" }, + 506: { message: "RewardsInvalid" }, - 8: { message: "StakeNotFound" }, + 509: { message: "StakeNotFound" }, - 9: { message: "InvalidTime" }, + 510: { message: "InvalidTime" }, - 10: { message: "DistributionExists" }, + 511: { message: "DistributionExists" }, - 11: { message: "InvalidRewardAmount" }, + 512: { message: "InvalidRewardAmount" }, - 12: { message: "InvalidMaxComplexity" }, + 513: { message: "InvalidMaxComplexity" }, - 13: { message: "DistributionNotFound" }, + 514: { message: "DistributionNotFound" }, - 14: { message: "AdminNotSet" }, + 515: { message: "AdminNotSet" }, + + 516: { message: "ContractMathError" }, + + 517: { message: "RewardCurveDoesNotExist" }, + + 518: { message: "SameAdmin" }, + + 519: { message: "NoAdminChangeInPlace" }, + + 520: { message: "AdminChangeExpired" }, }; export interface ConfigResponse { @@ -69,9 +129,7 @@ export interface ConfigResponse { } export interface StakedResponse { - last_reward_time: u64; stakes: Array; - total_stake: i128; } export interface AnnualizedReward { @@ -137,6 +195,62 @@ export interface BondingInfo { total_stake: i128; } +/** + * Curve types + */ +export type Curve = + | { tag: "Constant"; values: readonly [u128] } + | { tag: "SaturatingLinear"; values: readonly [SaturatingLinear] } + | { tag: "PiecewiseLinear"; values: readonly [PiecewiseLinear] }; + +/** + * Saturating Linear + * $$f(x)=\begin{cases} + * [min(y) * amount], & \text{if x <= $x_1$ } \\\\ + * [y * amount], & \text{if $x_1$ >= x <= $x_2$ } \\\\ + * [max(y) * amount], & \text{if x >= $x_2$ } + * \end{cases}$$ + * + * min_y for all x <= min_x, max_y for all x >= max_x, linear in between + */ +export interface SaturatingLinear { + /** + * time when curve has fully saturated + */ + max_x: u64; + /** + * max value at saturated time + */ + max_y: u128; + /** + * time when curve start + */ + min_x: u64; + /** + * min value at start time + */ + min_y: u128; +} + +/** + * This is a generalization of SaturatingLinear, steps must be arranged with increasing time [`u64`]. + * Any point before first step gets the first value, after last step the last value. + * Otherwise, it is a linear interpolation between the two closest points. + * Vec of length 1 -> [`Constant`](Curve::Constant) . + * Vec of length 2 -> [`SaturatingLinear`] . + */ +export interface Step { + time: u64; + value: u128; +} + +export interface PiecewiseLinear { + /** + * steps + */ + steps: Array; +} + export interface TokenInitInfo { token_a: string; token_b: string; @@ -161,6 +275,16 @@ export interface LiquidityPoolInitInfo { token_init_info: TokenInitInfo; } +export interface AdminChange { + new_admin: string; + time_limit: Option; +} + +export interface AutoUnstakeInfo { + stake_amount: i128; + stake_timestamp: u64; +} + export enum PoolType { Xyk = 0, Stable = 1, @@ -168,26 +292,10 @@ export enum PoolType { export interface Client { /** - * Construct and simulate a initialize transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Construct and simulate a bond transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ - initialize: ( - { - admin, - lp_token, - min_bond, - min_reward, - manager, - owner, - max_complexity, - }: { - admin: string; - lp_token: string; - min_bond: i128; - min_reward: i128; - manager: string; - owner: string; - max_complexity: u32; - }, + bond: ( + { sender, tokens }: { sender: string; tokens: i128 }, options?: { /** * The fee to pay for the transaction. Default: BASE_FEE @@ -207,10 +315,14 @@ export interface Client { ) => Promise>; /** - * Construct and simulate a bond transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Construct and simulate a unbond transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ - bond: ( - { sender, tokens }: { sender: string; tokens: i128 }, + unbond: ( + { + sender, + stake_amount, + stake_timestamp, + }: { sender: string; stake_amount: i128; stake_timestamp: u64 }, options?: { /** * The fee to pay for the transaction. Default: BASE_FEE @@ -230,9 +342,9 @@ export interface Client { ) => Promise>; /** - * Construct and simulate a unbond transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Construct and simulate a unbond_deprecated transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ - unbond: ( + unbond_deprecated: ( { sender, stake_amount, @@ -282,12 +394,28 @@ export interface Client { /** * Construct and simulate a distribute_rewards transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ - distribute_rewards: ( - { - sender, - amount, - reward_token, - }: { sender: string; amount: i128; reward_token: string }, + distribute_rewards: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>; + + /** + * Construct and simulate a withdraw_rewards transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + withdraw_rewards: ( + { sender }: { sender: string }, options?: { /** * The fee to pay for the transaction. Default: BASE_FEE @@ -307,9 +435,9 @@ export interface Client { ) => Promise>; /** - * Construct and simulate a withdraw_rewards transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Construct and simulate a withdraw_rewards_deprecated transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ - withdraw_rewards: ( + withdraw_rewards_deprecated: ( { sender }: { sender: string }, options?: { /** @@ -329,6 +457,78 @@ export interface Client { } ) => Promise>; + /** + * Construct and simulate a fund_distribution transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + fund_distribution: ( + { + sender, + start_time, + distribution_duration, + token_address, + token_amount, + }: { + sender: string; + start_time: u64; + distribution_duration: u64; + token_address: string; + token_amount: i128; + }, + options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + } + ) => Promise>; + + /** + * Construct and simulate a update_config transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + update_config: ( + { + lp_token, + min_bond, + min_reward, + manager, + owner, + max_complexity, + }: { + lp_token: Option; + min_bond: Option; + min_reward: Option; + manager: Option; + owner: Option; + max_complexity: Option; + }, + options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + } + ) => Promise>>; + /** * Construct and simulate a query_config transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ @@ -412,6 +612,26 @@ export interface Client { simulate?: boolean; }) => Promise>; + /** + * Construct and simulate a query_annualized_rewards transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + query_annualized_rewards: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>; + /** * Construct and simulate a query_withdrawable_rewards transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ @@ -435,6 +655,138 @@ export interface Client { } ) => Promise>; + /** + * Construct and simulate a query_withdrawable_rewards_dep transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + query_withdrawable_rewards_dep: ( + { user }: { user: string }, + options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + } + ) => Promise>; + + /** + * Construct and simulate a query_distributed_rewards transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + query_distributed_rewards: ( + { asset }: { asset: string }, + options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + } + ) => Promise>; + + /** + * Construct and simulate a query_undistributed_rewards transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + query_undistributed_rewards: ( + { asset }: { asset: string }, + options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + } + ) => Promise>; + + /** + * Construct and simulate a propose_admin transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + propose_admin: ( + { new_admin, time_limit }: { new_admin: string; time_limit: Option }, + options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + } + ) => Promise>>; + + /** + * Construct and simulate a revoke_admin_change transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + revoke_admin_change: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>>; + + /** + * Construct and simulate a accept_admin transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + accept_admin: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>>; + /** * Construct and simulate a migrate_admin_key transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ @@ -477,9 +829,87 @@ export interface Client { simulate?: boolean; } ) => Promise>; + + /** + * Construct and simulate a query_version transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + query_version: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>; + + /** + * Construct and simulate a add_new_key_to_storage transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + add_new_key_to_storage: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>>; + + /** + * Construct and simulate a migrate_distributions transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + migrate_distributions: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>; } export class Client extends ContractClient { static async deploy( + /** Constructor/Initialization Args for the contract's `__constructor` method */ + { + admin, + lp_token, + min_bond, + min_reward, + manager, + owner, + max_complexity, + }: { + admin: string; + lp_token: string; + min_bond: i128; + min_reward: i128; + manager: string; + owner: string; + max_complexity: u32; + }, /** Options for initalizing a Client as well as for calling a method, with extras specific to deploying. */ options: MethodOptions & Omit & { @@ -491,28 +921,48 @@ export class Client extends ContractClient { format?: "hex" | "base64"; } ): Promise> { - return ContractClient.deploy(null, options); + return ContractClient.deploy( + { admin, lp_token, min_bond, min_reward, manager, owner, max_complexity }, + options + ); } constructor(public readonly options: ContractClientOptions) { super( new ContractSpec([ - "AAAAAAAAAAAAAAAKaW5pdGlhbGl6ZQAAAAAABwAAAAAAAAAFYWRtaW4AAAAAAAATAAAAAAAAAAhscF90b2tlbgAAABMAAAAAAAAACG1pbl9ib25kAAAACwAAAAAAAAAKbWluX3Jld2FyZAAAAAAACwAAAAAAAAAHbWFuYWdlcgAAAAATAAAAAAAAAAVvd25lcgAAAAAAABMAAAAAAAAADm1heF9jb21wbGV4aXR5AAAAAAAEAAAAAA==", "AAAAAAAAAAAAAAAEYm9uZAAAAAIAAAAAAAAABnNlbmRlcgAAAAAAEwAAAAAAAAAGdG9rZW5zAAAAAAALAAAAAA==", "AAAAAAAAAAAAAAAGdW5ib25kAAAAAAADAAAAAAAAAAZzZW5kZXIAAAAAABMAAAAAAAAADHN0YWtlX2Ftb3VudAAAAAsAAAAAAAAAD3N0YWtlX3RpbWVzdGFtcAAAAAAGAAAAAA==", + "AAAAAAAAAAAAAAARdW5ib25kX2RlcHJlY2F0ZWQAAAAAAAADAAAAAAAAAAZzZW5kZXIAAAAAABMAAAAAAAAADHN0YWtlX2Ftb3VudAAAAAsAAAAAAAAAD3N0YWtlX3RpbWVzdGFtcAAAAAAGAAAAAA==", "AAAAAAAAAAAAAAAYY3JlYXRlX2Rpc3RyaWJ1dGlvbl9mbG93AAAAAgAAAAAAAAAGc2VuZGVyAAAAAAATAAAAAAAAAAVhc3NldAAAAAAAABMAAAAA", - "AAAAAAAAAAAAAAASZGlzdHJpYnV0ZV9yZXdhcmRzAAAAAAADAAAAAAAAAAZzZW5kZXIAAAAAABMAAAAAAAAABmFtb3VudAAAAAAACwAAAAAAAAAMcmV3YXJkX3Rva2VuAAAAEwAAAAA=", + "AAAAAAAAAAAAAAASZGlzdHJpYnV0ZV9yZXdhcmRzAAAAAAAAAAAAAA==", "AAAAAAAAAAAAAAAQd2l0aGRyYXdfcmV3YXJkcwAAAAEAAAAAAAAABnNlbmRlcgAAAAAAEwAAAAA=", + "AAAAAAAAAAAAAAAbd2l0aGRyYXdfcmV3YXJkc19kZXByZWNhdGVkAAAAAAEAAAAAAAAABnNlbmRlcgAAAAAAEwAAAAA=", + "AAAAAAAAAAAAAAARZnVuZF9kaXN0cmlidXRpb24AAAAAAAAFAAAAAAAAAAZzZW5kZXIAAAAAABMAAAAAAAAACnN0YXJ0X3RpbWUAAAAAAAYAAAAAAAAAFWRpc3RyaWJ1dGlvbl9kdXJhdGlvbgAAAAAAAAYAAAAAAAAADXRva2VuX2FkZHJlc3MAAAAAAAATAAAAAAAAAAx0b2tlbl9hbW91bnQAAAALAAAAAA==", + "AAAAAAAAAAAAAAANdXBkYXRlX2NvbmZpZwAAAAAAAAYAAAAAAAAACGxwX3Rva2VuAAAD6AAAABMAAAAAAAAACG1pbl9ib25kAAAD6AAAAAsAAAAAAAAACm1pbl9yZXdhcmQAAAAAA+gAAAALAAAAAAAAAAdtYW5hZ2VyAAAAA+gAAAATAAAAAAAAAAVvd25lcgAAAAAAA+gAAAATAAAAAAAAAA5tYXhfY29tcGxleGl0eQAAAAAD6AAAAAQAAAABAAAD6QAAB9AAAAAGQ29uZmlnAAAAAAfQAAAADUNvbnRyYWN0RXJyb3IAAAA=", "AAAAAAAAAAAAAAAMcXVlcnlfY29uZmlnAAAAAAAAAAEAAAfQAAAADkNvbmZpZ1Jlc3BvbnNlAAA=", "AAAAAAAAAAAAAAALcXVlcnlfYWRtaW4AAAAAAAAAAAEAAAAT", "AAAAAAAAAAAAAAAMcXVlcnlfc3Rha2VkAAAAAQAAAAAAAAAHYWRkcmVzcwAAAAATAAAAAQAAB9AAAAAOU3Rha2VkUmVzcG9uc2UAAA==", "AAAAAAAAAAAAAAAScXVlcnlfdG90YWxfc3Rha2VkAAAAAAAAAAAAAQAAAAs=", + "AAAAAAAAAAAAAAAYcXVlcnlfYW5udWFsaXplZF9yZXdhcmRzAAAAAAAAAAEAAAfQAAAAGUFubnVhbGl6ZWRSZXdhcmRzUmVzcG9uc2UAAAA=", "AAAAAAAAAAAAAAAacXVlcnlfd2l0aGRyYXdhYmxlX3Jld2FyZHMAAAAAAAEAAAAAAAAABHVzZXIAAAATAAAAAQAAB9AAAAAbV2l0aGRyYXdhYmxlUmV3YXJkc1Jlc3BvbnNlAA==", + "AAAAAAAAAAAAAAAecXVlcnlfd2l0aGRyYXdhYmxlX3Jld2FyZHNfZGVwAAAAAAABAAAAAAAAAAR1c2VyAAAAEwAAAAEAAAfQAAAAG1dpdGhkcmF3YWJsZVJld2FyZHNSZXNwb25zZQA=", + "AAAAAAAAAAAAAAAZcXVlcnlfZGlzdHJpYnV0ZWRfcmV3YXJkcwAAAAAAAAEAAAAAAAAABWFzc2V0AAAAAAAAEwAAAAEAAAAK", + "AAAAAAAAAAAAAAAbcXVlcnlfdW5kaXN0cmlidXRlZF9yZXdhcmRzAAAAAAEAAAAAAAAABWFzc2V0AAAAAAAAEwAAAAEAAAAK", + "AAAAAAAAAAAAAAANcHJvcG9zZV9hZG1pbgAAAAAAAAIAAAAAAAAACW5ld19hZG1pbgAAAAAAABMAAAAAAAAACnRpbWVfbGltaXQAAAAAA+gAAAAGAAAAAQAAA+kAAAATAAAH0AAAAA1Db250cmFjdEVycm9yAAAA", + "AAAAAAAAAAAAAAATcmV2b2tlX2FkbWluX2NoYW5nZQAAAAAAAAAAAQAAA+kAAAPtAAAAAAAAB9AAAAANQ29udHJhY3RFcnJvcgAAAA==", + "AAAAAAAAAAAAAAAMYWNjZXB0X2FkbWluAAAAAAAAAAEAAAPpAAAAEwAAB9AAAAANQ29udHJhY3RFcnJvcgAAAA==", "AAAAAAAAAAAAAAARbWlncmF0ZV9hZG1pbl9rZXkAAAAAAAAAAAAAAQAAA+kAAAPtAAAAAAAAB9AAAAANQ29udHJhY3RFcnJvcgAAAA==", + "AAAAAAAAAAAAAAANX19jb25zdHJ1Y3RvcgAAAAAAAAcAAAAAAAAABWFkbWluAAAAAAAAEwAAAAAAAAAIbHBfdG9rZW4AAAATAAAAAAAAAAhtaW5fYm9uZAAAAAsAAAAAAAAACm1pbl9yZXdhcmQAAAAAAAsAAAAAAAAAB21hbmFnZXIAAAAAEwAAAAAAAAAFb3duZXIAAAAAAAATAAAAAAAAAA5tYXhfY29tcGxleGl0eQAAAAAABAAAAAA=", "AAAAAAAAAAAAAAAGdXBkYXRlAAAAAAABAAAAAAAAAA1uZXdfd2FzbV9oYXNoAAAAAAAD7gAAACAAAAAA", - "AAAAAgAAAAAAAAAAAAAAE0Rpc3RyaWJ1dGlvbkRhdGFLZXkAAAAAAgAAAAEAAAAAAAAADVJld2FyZEhpc3RvcnkAAAAAAAABAAAAEwAAAAAAAAAAAAAAElRvdGFsU3Rha2VkSGlzdG9yeQAA", - "AAAABAAAAAAAAAAAAAAADUNvbnRyYWN0RXJyb3IAAAAAAAAOAAAAAAAAABJBbHJlYWR5SW5pdGlhbGl6ZWQAAAAAAAEAAAAAAAAADkludmFsaWRNaW5Cb25kAAAAAAACAAAAAAAAABBJbnZhbGlkTWluUmV3YXJkAAAAAwAAAAAAAAALSW52YWxpZEJvbmQAAAAABAAAAAAAAAAMVW5hdXRob3JpemVkAAAABQAAAAAAAAASTWluUmV3YXJkTm90RW5vdWdoAAAAAAAGAAAAAAAAAA5SZXdhcmRzSW52YWxpZAAAAAAABwAAAAAAAAANU3Rha2VOb3RGb3VuZAAAAAAAAAgAAAAAAAAAC0ludmFsaWRUaW1lAAAAAAkAAAAAAAAAEkRpc3RyaWJ1dGlvbkV4aXN0cwAAAAAACgAAAAAAAAATSW52YWxpZFJld2FyZEFtb3VudAAAAAALAAAAAAAAABRJbnZhbGlkTWF4Q29tcGxleGl0eQAAAAwAAAAAAAAAFERpc3RyaWJ1dGlvbk5vdEZvdW5kAAAADQAAAAAAAAALQWRtaW5Ob3RTZXQAAAAADg==", + "AAAAAAAAAAAAAAANcXVlcnlfdmVyc2lvbgAAAAAAAAAAAAABAAAAEA==", + "AAAAAAAAAAAAAAAWYWRkX25ld19rZXlfdG9fc3RvcmFnZQAAAAAAAAAAAAEAAAPpAAAD7QAAAAAAAAfQAAAADUNvbnRyYWN0RXJyb3IAAAA=", + "AAAAAAAAAAAAAAAVbWlncmF0ZV9kaXN0cmlidXRpb25zAAAAAAAAAAAAAAA=", + "AAAAAQAAAAAAAAAAAAAAFVdpdGhkcmF3QWRqdXN0bWVudEtleQAAAAAAAAIAAAAAAAAABWFzc2V0AAAAAAAAEwAAAAAAAAAEdXNlcgAAABM=", + "AAAAAgAAAAAAAAAAAAAAE0Rpc3RyaWJ1dGlvbkRhdGFLZXkAAAAABQAAAAEAAAAAAAAADVJld2FyZEhpc3RvcnkAAAAAAAABAAAAEwAAAAAAAAAAAAAAElRvdGFsU3Rha2VkSGlzdG9yeQAAAAAAAQAAAAAAAAAFQ3VydmUAAAAAAAABAAAAEwAAAAEAAAAAAAAADERpc3RyaWJ1dGlvbgAAAAEAAAATAAAAAQAAAAAAAAASV2l0aGRyYXdBZGp1c3RtZW50AAAAAAABAAAH0AAAABVXaXRoZHJhd0FkanVzdG1lbnRLZXkAAAA=", + "AAAAAQAAAAAAAAAAAAAADERpc3RyaWJ1dGlvbgAAAAYAAAAVQm9udXMgcGVyIHN0YWtpbmcgZGF5AAAAAAAAEWJvbnVzX3Blcl9kYXlfYnBzAAAAAAAABgAAACtUb3RhbCByZXdhcmRzIGRpc3RyaWJ1dGVkIGJ5IHRoaXMgY29udHJhY3QuAAAAABFkaXN0cmlidXRlZF90b3RhbAAAAAAAAAoAAAAjTWF4IGJvbnVzIGZvciBzdGFraW5nIGFmdGVyIDYwIGRheXMAAAAADW1heF9ib251c19icHMAAAAAAAAGAAAAXlNoYXJlcyB3aGljaCB3ZXJlIG5vdCBmdWxseSBkaXN0cmlidXRlZCBvbiBwcmV2aW91cyBkaXN0cmlidXRpb25zLCBhbmQgc2hvdWxkIGJlIHJlZGlzdHJpYnV0ZWQAAAAAAA9zaGFyZXNfbGVmdG92ZXIAAAAABgAAACVIb3cgbWFueSBzaGFyZXMgaXMgc2luZ2xlIHBvaW50IHdvcnRoAAAAAAAAEHNoYXJlc19wZXJfcG9pbnQAAAAKAAAAIFRvdGFsIHJld2FyZHMgbm90IHlldCB3aXRoZHJhd24uAAAAEndpdGhkcmF3YWJsZV90b3RhbAAAAAAACg==", + "AAAAAQAAAAAAAAAAAAAAEldpdGhkcmF3QWRqdXN0bWVudAAAAAAAAgAAAUVSZXByZXNlbnRzIGEgY29ycmVjdGlvbiB0byB0aGUgcmV3YXJkIHBvaW50cyBmb3IgdGhlIHVzZXIuIFRoaXMgY2FuIGJlIHBvc2l0aXZlIG9yIG5lZ2F0aXZlLgpBIHBvc2l0aXZlIHZhbHVlIGluZGljYXRlcyB0aGF0IHRoZSB1c2VyIHNob3VsZCByZWNlaXZlIGFkZGl0aW9uYWwgcG9pbnRzIChlLmcuLCBmcm9tIGEgYm9udXMgb3IgYW4gZXJyb3IgY29ycmVjdGlvbiksCndoaWxlIGEgbmVnYXRpdmUgdmFsdWUgc2lnbmlmaWVzIGEgcmVkdWN0aW9uIChlLmcuLCBkdWUgdG8gYSBwZW5hbHR5IG9yIGFuIGFkanVzdG1lbnQgZm9yIHBhc3Qgb3Zlci1hbGxvY2F0aW9ucykuAAAAAAAAEXNoYXJlc19jb3JyZWN0aW9uAAAAAAAACwAAAOJSZXByZXNlbnRzIHRoZSB0b3RhbCBhbW91bnQgb2YgcmV3YXJkcyB0aGF0IHRoZSB1c2VyIGhhcyB3aXRoZHJhd24gc28gZmFyLgpUaGlzIHZhbHVlIGVuc3VyZXMgdGhhdCBhIHVzZXIgZG9lc24ndCB3aXRoZHJhdyBtb3JlIHRoYW4gdGhleSBhcmUgb3dlZCBhbmQgaXMgdXNlZCB0bwpjYWxjdWxhdGUgdGhlIG5ldCByZXdhcmRzIGEgdXNlciBjYW4gd2l0aGRyYXcgYXQgYW55IGdpdmVuIHRpbWUuAAAAAAARd2l0aGRyYXduX3Jld2FyZHMAAAAAAAAK", + "AAAABAAAAAAAAAAAAAAADUNvbnRyYWN0RXJyb3IAAAAAAAATAAAAAAAAABJBbHJlYWR5SW5pdGlhbGl6ZWQAAAAAAfQAAAAAAAAADkludmFsaWRNaW5Cb25kAAAAAAH1AAAAAAAAABBJbnZhbGlkTWluUmV3YXJkAAAB9gAAAAAAAAALSW52YWxpZEJvbmQAAAAB9wAAAAAAAAAMVW5hdXRob3JpemVkAAAB+AAAAAAAAAASTWluUmV3YXJkTm90RW5vdWdoAAAAAAH5AAAAAAAAAA5SZXdhcmRzSW52YWxpZAAAAAAB+gAAAAAAAAANU3Rha2VOb3RGb3VuZAAAAAAAAf0AAAAAAAAAC0ludmFsaWRUaW1lAAAAAf4AAAAAAAAAEkRpc3RyaWJ1dGlvbkV4aXN0cwAAAAAB/wAAAAAAAAATSW52YWxpZFJld2FyZEFtb3VudAAAAAIAAAAAAAAAABRJbnZhbGlkTWF4Q29tcGxleGl0eQAAAgEAAAAAAAAAFERpc3RyaWJ1dGlvbk5vdEZvdW5kAAACAgAAAAAAAAALQWRtaW5Ob3RTZXQAAAACAwAAAAAAAAARQ29udHJhY3RNYXRoRXJyb3IAAAAAAAIEAAAAAAAAABdSZXdhcmRDdXJ2ZURvZXNOb3RFeGlzdAAAAAIFAAAAAAAAAAlTYW1lQWRtaW4AAAAAAAIGAAAAAAAAABROb0FkbWluQ2hhbmdlSW5QbGFjZQAAAgcAAAAAAAAAEkFkbWluQ2hhbmdlRXhwaXJlZAAAAAACCA==", "AAAAAQAAAAAAAAAAAAAADkNvbmZpZ1Jlc3BvbnNlAAAAAAABAAAAAAAAAAZjb25maWcAAAAAB9AAAAAGQ29uZmlnAAA=", - "AAAAAQAAAAAAAAAAAAAADlN0YWtlZFJlc3BvbnNlAAAAAAADAAAAAAAAABBsYXN0X3Jld2FyZF90aW1lAAAABgAAAAAAAAAGc3Rha2VzAAAAAAPqAAAH0AAAAAVTdGFrZQAAAAAAAAAAAAALdG90YWxfc3Rha2UAAAAACw==", + "AAAAAQAAAAAAAAAAAAAADlN0YWtlZFJlc3BvbnNlAAAAAAABAAAAAAAAAAZzdGFrZXMAAAAAA+oAAAfQAAAABVN0YWtlAAAA", "AAAAAQAAAAAAAAAAAAAAEEFubnVhbGl6ZWRSZXdhcmQAAAACAAAAAAAAAAZhbW91bnQAAAAAABAAAAAAAAAABWFzc2V0AAAAAAAAEw==", "AAAAAQAAAAAAAAAAAAAAGUFubnVhbGl6ZWRSZXdhcmRzUmVzcG9uc2UAAAAAAAABAAAAAAAAAAdyZXdhcmRzAAAAA+oAAAfQAAAAEEFubnVhbGl6ZWRSZXdhcmQ=", "AAAAAQAAAAAAAAAAAAAAEldpdGhkcmF3YWJsZVJld2FyZAAAAAAAAgAAAAAAAAAOcmV3YXJkX2FkZHJlc3MAAAAAABMAAAAAAAAADXJld2FyZF9hbW91bnQAAAAAAAAK", @@ -520,27 +970,47 @@ export class Client extends ContractClient { "AAAAAQAAAAAAAAAAAAAABkNvbmZpZwAAAAAABgAAAAAAAAAIbHBfdG9rZW4AAAATAAAAAAAAAAdtYW5hZ2VyAAAAABMAAAAAAAAADm1heF9jb21wbGV4aXR5AAAAAAAEAAAAAAAAAAhtaW5fYm9uZAAAAAsAAAAAAAAACm1pbl9yZXdhcmQAAAAAAAsAAAAAAAAABW93bmVyAAAAAAAAEw==", "AAAAAQAAAAAAAAAAAAAABVN0YWtlAAAAAAAAAgAAABtUaGUgYW1vdW50IG9mIHN0YWtlZCB0b2tlbnMAAAAABXN0YWtlAAAAAAAACwAAACVUaGUgdGltZXN0YW1wIHdoZW4gdGhlIHN0YWtlIHdhcyBtYWRlAAAAAAAAD3N0YWtlX3RpbWVzdGFtcAAAAAAG", "AAAAAQAAAAAAAAAAAAAAC0JvbmRpbmdJbmZvAAAAAAQAAAAnTGFzdCB0aW1lIHdoZW4gdXNlciBoYXMgY2xhaW1lZCByZXdhcmRzAAAAABBsYXN0X3Jld2FyZF90aW1lAAAABgAAAZpUaGUgcmV3YXJkcyBkZWJ0IGlzIGEgbWVjaGFuaXNtIHRvIGRldGVybWluZSBob3cgbXVjaCBhIHVzZXIgaGFzIGFscmVhZHkgYmVlbiBjcmVkaXRlZCBpbiB0ZXJtcyBvZiBzdGFraW5nIHJld2FyZHMuCldoZW5ldmVyIGEgdXNlciBkZXBvc2l0cyBvciB3aXRoZHJhd3Mgc3Rha2VkIHRva2VucyB0byB0aGUgcG9vbCwgdGhlIHJld2FyZHMgZm9yIHRoZSB1c2VyIGlzIHVwZGF0ZWQgYmFzZWQgb24gdGhlCmFjY3VtdWxhdGVkIHJld2FyZHMgcGVyIHNoYXJlLCBhbmQgdGhlIGRpZmZlcmVuY2UgaXMgc3RvcmVkIGFzIHJld2FyZCBkZWJ0LiBXaGVuIGNsYWltaW5nIHJld2FyZHMsIHRoaXMgcmV3YXJkIGRlYnQKaXMgdXNlZCB0byBkZXRlcm1pbmUgaG93IG11Y2ggcmV3YXJkcyBhIHVzZXIgY2FuIGFjdHVhbGx5IGNsYWltLgAAAAAAC3Jld2FyZF9kZWJ0AAAAAAoAAAAnVmVjIG9mIHN0YWtlcyBzb3J0ZWQgYnkgc3Rha2UgdGltZXN0YW1wAAAAAAZzdGFrZXMAAAAAA+oAAAfQAAAABVN0YWtlAAAAAAAAHVRvdGFsIGFtb3VudCBvZiBzdGFrZWQgdG9rZW5zAAAAAAAAC3RvdGFsX3N0YWtlAAAAAAs=", + "AAAAAgAAAAtDdXJ2ZSB0eXBlcwAAAAAAAAAABUN1cnZlAAAAAAAAAwAAAAEAAAAxQ29uc3RhbiBjdXJ2ZSwgaXQgd2lsbCBhbHdheXMgaGF2ZSB0aGUgc2FtZSB2YWx1ZQAAAAAAAAhDb25zdGFudAAAAAEAAAAKAAAAAQAAAE5MaW5lYXIgY3VydmUgdGhhdCBncm93IGxpbmVhcmx5IGJ1dCBsYXRlcgp0ZW5kcyB0byBhIGNvbnN0YW50IHNhdHVyYXRlZCB2YWx1ZS4AAAAAABBTYXR1cmF0aW5nTGluZWFyAAAAAQAAB9AAAAAQU2F0dXJhdGluZ0xpbmVhcgAAAAEAAAAbQ3VydmUgd2l0aCBkaWZmZXJlbnQgc2xvcGVzAAAAAA9QaWVjZXdpc2VMaW5lYXIAAAAAAQAAB9AAAAAPUGllY2V3aXNlTGluZWFyAA==", + "AAAAAQAAAQ1TYXR1cmF0aW5nIExpbmVhcgokJGYoeCk9XGJlZ2lue2Nhc2VzfQpbbWluKHkpICogYW1vdW50XSwgICYgXHRleHR7aWYgeCA8PSAkeF8xJCB9IFxcXFwKW3kgKiBhbW91bnRdLCAgJiBcdGV4dHtpZiAkeF8xJCA+PSB4IDw9ICR4XzIkIH0gXFxcXApbbWF4KHkpICogYW1vdW50XSwgICYgXHRleHR7aWYgeCA+PSAkeF8yJCB9ClxlbmR7Y2FzZXN9JCQKCm1pbl95IGZvciBhbGwgeCA8PSBtaW5feCwgbWF4X3kgZm9yIGFsbCB4ID49IG1heF94LCBsaW5lYXIgaW4gYmV0d2VlbgAAAAAAAAAAAAAQU2F0dXJhdGluZ0xpbmVhcgAAAAQAAAAjdGltZSB3aGVuIGN1cnZlIGhhcyBmdWxseSBzYXR1cmF0ZWQAAAAABW1heF94AAAAAAAABgAAABttYXggdmFsdWUgYXQgc2F0dXJhdGVkIHRpbWUAAAAABW1heF95AAAAAAAACgAAABV0aW1lIHdoZW4gY3VydmUgc3RhcnQAAAAAAAAFbWluX3gAAAAAAAAGAAAAF21pbiB2YWx1ZSBhdCBzdGFydCB0aW1lAAAAAAVtaW5feQAAAAAAAAo=", + "AAAAAQAAAVlUaGlzIGlzIGEgZ2VuZXJhbGl6YXRpb24gb2YgU2F0dXJhdGluZ0xpbmVhciwgc3RlcHMgbXVzdCBiZSBhcnJhbmdlZCB3aXRoIGluY3JlYXNpbmcgdGltZSBbYHU2NGBdLgpBbnkgcG9pbnQgYmVmb3JlIGZpcnN0IHN0ZXAgZ2V0cyB0aGUgZmlyc3QgdmFsdWUsIGFmdGVyIGxhc3Qgc3RlcCB0aGUgbGFzdCB2YWx1ZS4KT3RoZXJ3aXNlLCBpdCBpcyBhIGxpbmVhciBpbnRlcnBvbGF0aW9uIGJldHdlZW4gdGhlIHR3byBjbG9zZXN0IHBvaW50cy4KVmVjIG9mIGxlbmd0aCAxIC0+IFtgQ29uc3RhbnRgXShDdXJ2ZTo6Q29uc3RhbnQpIC4KVmVjIG9mIGxlbmd0aCAyIC0+IFtgU2F0dXJhdGluZ0xpbmVhcmBdIC4AAAAAAAAAAAAABFN0ZXAAAAACAAAAAAAAAAR0aW1lAAAABgAAAAAAAAAFdmFsdWUAAAAAAAAK", + "AAAAAQAAAAAAAAAAAAAAD1BpZWNld2lzZUxpbmVhcgAAAAABAAAABXN0ZXBzAAAAAAAABXN0ZXBzAAAAAAAD6gAAB9AAAAAEU3RlcA==", "AAAAAQAAAAAAAAAAAAAADVRva2VuSW5pdEluZm8AAAAAAAACAAAAAAAAAAd0b2tlbl9hAAAAABMAAAAAAAAAB3Rva2VuX2IAAAAAEw==", "AAAAAQAAAAAAAAAAAAAADVN0YWtlSW5pdEluZm8AAAAAAAAEAAAAAAAAAAdtYW5hZ2VyAAAAABMAAAAAAAAADm1heF9jb21wbGV4aXR5AAAAAAAEAAAAAAAAAAhtaW5fYm9uZAAAAAsAAAAAAAAACm1pbl9yZXdhcmQAAAAAAAs=", "AAAAAQAAAAAAAAAAAAAAFUxpcXVpZGl0eVBvb2xJbml0SW5mbwAAAAAAAAkAAAAAAAAABWFkbWluAAAAAAAAEwAAAAAAAAAUZGVmYXVsdF9zbGlwcGFnZV9icHMAAAAHAAAAAAAAAA1mZWVfcmVjaXBpZW50AAAAAAAAEwAAAAAAAAAYbWF4X2FsbG93ZWRfc2xpcHBhZ2VfYnBzAAAABwAAAAAAAAAWbWF4X2FsbG93ZWRfc3ByZWFkX2JwcwAAAAAABwAAAAAAAAAQbWF4X3JlZmVycmFsX2JwcwAAAAcAAAAAAAAAD3N0YWtlX2luaXRfaW5mbwAAAAfQAAAADVN0YWtlSW5pdEluZm8AAAAAAAAAAAAADHN3YXBfZmVlX2JwcwAAAAcAAAAAAAAAD3Rva2VuX2luaXRfaW5mbwAAAAfQAAAADVRva2VuSW5pdEluZm8AAAA=", + "AAAAAQAAAAAAAAAAAAAAC0FkbWluQ2hhbmdlAAAAAAIAAAAAAAAACW5ld19hZG1pbgAAAAAAABMAAAAAAAAACnRpbWVfbGltaXQAAAAAA+gAAAAG", + "AAAAAQAAAAAAAAAAAAAAD0F1dG9VbnN0YWtlSW5mbwAAAAACAAAAAAAAAAxzdGFrZV9hbW91bnQAAAALAAAAAAAAAA9zdGFrZV90aW1lc3RhbXAAAAAABg==", "AAAAAwAAAAAAAAAAAAAACFBvb2xUeXBlAAAAAgAAAAAAAAADWHlrAAAAAAAAAAAAAAAABlN0YWJsZQAAAAAAAQ==", ]), options ); } public readonly fromJSON = { - initialize: this.txFromJSON, bond: this.txFromJSON, unbond: this.txFromJSON, + unbond_deprecated: this.txFromJSON, create_distribution_flow: this.txFromJSON, distribute_rewards: this.txFromJSON, withdraw_rewards: this.txFromJSON, + withdraw_rewards_deprecated: this.txFromJSON, + fund_distribution: this.txFromJSON, + update_config: this.txFromJSON>, query_config: this.txFromJSON, query_admin: this.txFromJSON, query_staked: this.txFromJSON, query_total_staked: this.txFromJSON, + query_annualized_rewards: this.txFromJSON, query_withdrawable_rewards: this.txFromJSON, + query_withdrawable_rewards_dep: this + .txFromJSON, + query_distributed_rewards: this.txFromJSON, + query_undistributed_rewards: this.txFromJSON, + propose_admin: this.txFromJSON>, + revoke_admin_change: this.txFromJSON>, + accept_admin: this.txFromJSON>, migrate_admin_key: this.txFromJSON>, update: this.txFromJSON, + query_version: this.txFromJSON, + add_new_key_to_storage: this.txFromJSON>, + migrate_distributions: this.txFromJSON, }; } diff --git a/packages/contracts/src/phoenix-vesting/index.ts b/packages/contracts/src/phoenix-vesting/index.ts index 369de870..06ced0d2 100644 --- a/packages/contracts/src/phoenix-vesting/index.ts +++ b/packages/contracts/src/phoenix-vesting/index.ts @@ -31,75 +31,87 @@ if (typeof window !== "undefined") { } export const Errors = { - 0: { message: "Std" }, + 700: { message: "VestingNotFoundForAddress" }, - 1: { message: "VestingNotFoundForAddress" }, + 701: { message: "AllowanceNotFoundForGivenPair" }, - 2: { message: "AllowanceNotFoundForGivenPair" }, + 702: { message: "MinterNotFound" }, - 3: { message: "MinterNotFound" }, + 703: { message: "NoBalanceFoundForAddress" }, - 4: { message: "NoBalanceFoundForAddress" }, + 704: { message: "NoConfigFound" }, - 5: { message: "NoConfigFound" }, + 705: { message: "NoAdminFound" }, - 6: { message: "NoAdminFound" }, + 706: { message: "MissingBalance" }, - 7: { message: "MissingBalance" }, + 707: { message: "VestingComplexityTooHigh" }, - 8: { message: "VestingComplexityTooHigh" }, + 708: { message: "TotalVestedOverCapacity" }, - 9: { message: "TotalVestedOverCapacity" }, + 709: { message: "InvalidTransferAmount" }, - 10: { message: "InvalidTransferAmount" }, + 710: { message: "CantMoveVestingTokens" }, - 11: { message: "CantMoveVestingTokens" }, + 711: { message: "NotEnoughCapacity" }, - 12: { message: "NotEnoughCapacity" }, + 712: { message: "NotAuthorized" }, - 13: { message: "NotAuthorized" }, + 713: { message: "NeverFullyVested" }, - 14: { message: "NeverFullyVested" }, + 714: { message: "VestsMoreThanSent" }, - 15: { message: "VestsMoreThanSent" }, + 715: { message: "InvalidBurnAmount" }, - 16: { message: "InvalidBurnAmount" }, + 716: { message: "InvalidMintAmount" }, - 17: { message: "InvalidMintAmount" }, + 717: { message: "InvalidAllowanceAmount" }, - 18: { message: "InvalidAllowanceAmount" }, + 718: { message: "DuplicateInitialBalanceAddresses" }, - 19: { message: "DuplicateInitialBalanceAddresses" }, + 719: { message: "CurveError" }, - 20: { message: "CurveError" }, + 720: { message: "NoWhitelistFound" }, - 21: { message: "NoWhitelistFound" }, + 721: { message: "NoTokenInfoFound" }, - 22: { message: "NoTokenInfoFound" }, + 722: { message: "NoVestingComplexityValueFound" }, - 23: { message: "NoVestingComplexityValueFound" }, + 723: { message: "NoAddressesToAdd" }, - 24: { message: "NoAddressesToAdd" }, + 724: { message: "NoEnoughtTokensToStart" }, - 25: { message: "NoEnoughtTokensToStart" }, + 725: { message: "NotEnoughBalance" }, - 26: { message: "NotEnoughBalance" }, + 726: { message: "VestingBothPresent" }, - 27: { message: "VestingBothPresent" }, + 727: { message: "VestingNonePresent" }, - 28: { message: "VestingNonePresent" }, + 728: { message: "CurveConstant" }, - 29: { message: "CurveConstant" }, + 729: { message: "CurveSLNotDecreasing" }, - 30: { message: "CurveSLNotDecreasing" }, + 730: { message: "AlreadyInitialized" }, - 31: { message: "AlreadyInitialized" }, + 731: { message: "AdminNotFound" }, - 32: { message: "AdminNotFound" }, + 732: { message: "ContractMathError" }, - 33: { message: "ContractMathError" }, + 733: { message: "SameAdmin" }, + + 734: { message: "NoAdminChangeInPlace" }, + + 735: { message: "AdminChangeExpired" }, + + 745: { message: "SameTokenAddress" }, + + 746: { message: "InvalidMaxComplexity" }, }; +export interface Config { + is_with_minter: boolean; +} + export interface VestingTokenInfo { address: string; decimals: u32; @@ -219,76 +231,22 @@ export interface LiquidityPoolInitInfo { token_init_info: TokenInitInfo; } +export interface AdminChange { + new_admin: string; + time_limit: Option; +} + +export interface AutoUnstakeInfo { + stake_amount: i128; + stake_timestamp: u64; +} + export enum PoolType { Xyk = 0, Stable = 1, } export interface Client { - /** - * Construct and simulate a initialize transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. - */ - initialize: ( - { - admin, - vesting_token, - max_vesting_complexity, - }: { - admin: string; - vesting_token: VestingTokenInfo; - max_vesting_complexity: u32; - }, - options?: { - /** - * The fee to pay for the transaction. Default: BASE_FEE - */ - fee?: number; - - /** - * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT - */ - timeoutInSeconds?: number; - - /** - * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true - */ - simulate?: boolean; - } - ) => Promise>; - - /** - * Construct and simulate a initialize_with_minter transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. - */ - initialize_with_minter: ( - { - admin, - vesting_token, - max_vesting_complexity, - minter_info, - }: { - admin: string; - vesting_token: VestingTokenInfo; - max_vesting_complexity: u32; - minter_info: MinterInfo; - }, - options?: { - /** - * The fee to pay for the transaction. Default: BASE_FEE - */ - fee?: number; - - /** - * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT - */ - timeoutInSeconds?: number; - - /** - * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true - */ - simulate?: boolean; - } - ) => Promise>; - /** * Construct and simulate a create_vesting_schedules transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ @@ -536,6 +494,26 @@ export interface Client { simulate?: boolean; }) => Promise>; + /** + * Construct and simulate a query_config transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + query_config: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>; + /** * Construct and simulate a query_vesting_contract_balance transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ @@ -579,6 +557,52 @@ export interface Client { } ) => Promise>; + /** + * Construct and simulate a update_vesting_token transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + update_vesting_token: ( + { new_token_address }: { new_token_address: string }, + options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + } + ) => Promise>>; + + /** + * Construct and simulate a update_max_complexity transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + update_max_complexity: ( + { new_max_complexity }: { new_max_complexity: u32 }, + options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + } + ) => Promise>>; + /** * Construct and simulate a update transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ @@ -622,6 +646,69 @@ export interface Client { simulate?: boolean; }) => Promise>>; + /** + * Construct and simulate a propose_admin transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + propose_admin: ( + { new_admin, time_limit }: { new_admin: string; time_limit: Option }, + options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + } + ) => Promise>>; + + /** + * Construct and simulate a revoke_admin_change transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + revoke_admin_change: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>>; + + /** + * Construct and simulate a accept_admin transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + accept_admin: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>>; + /** * Construct and simulate a add_new_key_to_storage transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ @@ -641,9 +728,41 @@ export interface Client { */ simulate?: boolean; }) => Promise>>; + + /** + * Construct and simulate a query_version transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + */ + query_version: (options?: { + /** + * The fee to pay for the transaction. Default: BASE_FEE + */ + fee?: number; + + /** + * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT + */ + timeoutInSeconds?: number; + + /** + * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true + */ + simulate?: boolean; + }) => Promise>; } export class Client extends ContractClient { static async deploy( + /** Constructor/Initialization Args for the contract's `__constructor` method */ + { + admin, + vesting_token, + max_vesting_complexity, + minter_info, + }: { + admin: string; + vesting_token: VestingTokenInfo; + max_vesting_complexity: u32; + minter_info: Option; + }, /** Options for initalizing a Client as well as for calling a method, with extras specific to deploying. */ options: MethodOptions & Omit & { @@ -655,13 +774,14 @@ export class Client extends ContractClient { format?: "hex" | "base64"; } ): Promise> { - return ContractClient.deploy(null, options); + return ContractClient.deploy( + { admin, vesting_token, max_vesting_complexity, minter_info }, + options + ); } constructor(public readonly options: ContractClientOptions) { super( new ContractSpec([ - "AAAAAAAAAAAAAAAKaW5pdGlhbGl6ZQAAAAAAAwAAAAAAAAAFYWRtaW4AAAAAAAATAAAAAAAAAA12ZXN0aW5nX3Rva2VuAAAAAAAH0AAAABBWZXN0aW5nVG9rZW5JbmZvAAAAAAAAABZtYXhfdmVzdGluZ19jb21wbGV4aXR5AAAAAAAEAAAAAA==", - "AAAAAAAAAAAAAAAWaW5pdGlhbGl6ZV93aXRoX21pbnRlcgAAAAAABAAAAAAAAAAFYWRtaW4AAAAAAAATAAAAAAAAAA12ZXN0aW5nX3Rva2VuAAAAAAAH0AAAABBWZXN0aW5nVG9rZW5JbmZvAAAAAAAAABZtYXhfdmVzdGluZ19jb21wbGV4aXR5AAAAAAAEAAAAAAAAAAttaW50ZXJfaW5mbwAAAAfQAAAACk1pbnRlckluZm8AAAAAAAA=", "AAAAAAAAAAAAAAAYY3JlYXRlX3Zlc3Rpbmdfc2NoZWR1bGVzAAAAAQAAAAAAAAARdmVzdGluZ19zY2hlZHVsZXMAAAAAAAPqAAAH0AAAAA9WZXN0aW5nU2NoZWR1bGUAAAAAAA==", "AAAAAAAAAAAAAAAFY2xhaW0AAAAAAAACAAAAAAAAAAZzZW5kZXIAAAAAABMAAAAAAAAABWluZGV4AAAAAAAABgAAAAA=", "AAAAAAAAAAAAAAAEYnVybgAAAAIAAAAAAAAABnNlbmRlcgAAAAAAEwAAAAAAAAAGYW1vdW50AAAAAAAKAAAAAA==", @@ -673,12 +793,21 @@ export class Client extends ContractClient { "AAAAAAAAAAAAAAAWcXVlcnlfYWxsX3Zlc3RpbmdfaW5mbwAAAAAAAQAAAAAAAAAHYWRkcmVzcwAAAAATAAAAAQAAA+oAAAfQAAAAE1Zlc3RpbmdJbmZvUmVzcG9uc2UA", "AAAAAAAAAAAAAAAQcXVlcnlfdG9rZW5faW5mbwAAAAAAAAABAAAH0AAAABBWZXN0aW5nVG9rZW5JbmZv", "AAAAAAAAAAAAAAAMcXVlcnlfbWludGVyAAAAAAAAAAEAAAfQAAAACk1pbnRlckluZm8AAA==", + "AAAAAAAAAAAAAAAMcXVlcnlfY29uZmlnAAAAAAAAAAEAAAfQAAAABkNvbmZpZwAA", "AAAAAAAAAAAAAAAecXVlcnlfdmVzdGluZ19jb250cmFjdF9iYWxhbmNlAAAAAAAAAAAAAQAAAAs=", "AAAAAAAAAAAAAAAYcXVlcnlfYXZhaWxhYmxlX3RvX2NsYWltAAAAAgAAAAAAAAAHYWRkcmVzcwAAAAATAAAAAAAAAAVpbmRleAAAAAAAAAYAAAABAAAACw==", + "AAAAAAAAAAAAAAAUdXBkYXRlX3Zlc3RpbmdfdG9rZW4AAAABAAAAAAAAABFuZXdfdG9rZW5fYWRkcmVzcwAAAAAAABMAAAABAAAD6QAAA+0AAAAAAAAH0AAAAA1Db250cmFjdEVycm9yAAAA", + "AAAAAAAAAAAAAAAVdXBkYXRlX21heF9jb21wbGV4aXR5AAAAAAAAAQAAAAAAAAASbmV3X21heF9jb21wbGV4aXR5AAAAAAAEAAAAAQAAA+kAAAPtAAAAAAAAB9AAAAANQ29udHJhY3RFcnJvcgAAAA==", "AAAAAAAAAAAAAAAGdXBkYXRlAAAAAAABAAAAAAAAAA1uZXdfd2FzbV9oYXNoAAAAAAAD7gAAACAAAAAA", "AAAAAAAAAAAAAAARbWlncmF0ZV9hZG1pbl9rZXkAAAAAAAAAAAAAAQAAA+kAAAPtAAAAAAAAB9AAAAANQ29udHJhY3RFcnJvcgAAAA==", + "AAAAAAAAAAAAAAANcHJvcG9zZV9hZG1pbgAAAAAAAAIAAAAAAAAACW5ld19hZG1pbgAAAAAAABMAAAAAAAAACnRpbWVfbGltaXQAAAAAA+gAAAAGAAAAAQAAA+kAAAATAAAH0AAAAA1Db250cmFjdEVycm9yAAAA", + "AAAAAAAAAAAAAAATcmV2b2tlX2FkbWluX2NoYW5nZQAAAAAAAAAAAQAAA+kAAAPtAAAAAAAAB9AAAAANQ29udHJhY3RFcnJvcgAAAA==", + "AAAAAAAAAAAAAAAMYWNjZXB0X2FkbWluAAAAAAAAAAEAAAPpAAAAEwAAB9AAAAANQ29udHJhY3RFcnJvcgAAAA==", + "AAAAAAAAAAAAAAANX19jb25zdHJ1Y3RvcgAAAAAAAAQAAAAAAAAABWFkbWluAAAAAAAAEwAAAAAAAAANdmVzdGluZ190b2tlbgAAAAAAB9AAAAAQVmVzdGluZ1Rva2VuSW5mbwAAAAAAAAAWbWF4X3Zlc3RpbmdfY29tcGxleGl0eQAAAAAABAAAAAAAAAALbWludGVyX2luZm8AAAAD6AAAB9AAAAAKTWludGVySW5mbwAAAAAAAA==", "AAAAAAAAAAAAAAAWYWRkX25ld19rZXlfdG9fc3RvcmFnZQAAAAAAAAAAAAEAAAPpAAAD7QAAAAAAAAfQAAAADUNvbnRyYWN0RXJyb3IAAAA=", - "AAAABAAAAAAAAAAAAAAADUNvbnRyYWN0RXJyb3IAAAAAAAAiAAAAAAAAAANTdGQAAAAAAAAAAAAAAAAZVmVzdGluZ05vdEZvdW5kRm9yQWRkcmVzcwAAAAAAAAEAAAAAAAAAHUFsbG93YW5jZU5vdEZvdW5kRm9yR2l2ZW5QYWlyAAAAAAAAAgAAAAAAAAAOTWludGVyTm90Rm91bmQAAAAAAAMAAAAAAAAAGE5vQmFsYW5jZUZvdW5kRm9yQWRkcmVzcwAAAAQAAAAAAAAADU5vQ29uZmlnRm91bmQAAAAAAAAFAAAAAAAAAAxOb0FkbWluRm91bmQAAAAGAAAAAAAAAA5NaXNzaW5nQmFsYW5jZQAAAAAABwAAAAAAAAAYVmVzdGluZ0NvbXBsZXhpdHlUb29IaWdoAAAACAAAAAAAAAAXVG90YWxWZXN0ZWRPdmVyQ2FwYWNpdHkAAAAACQAAAAAAAAAVSW52YWxpZFRyYW5zZmVyQW1vdW50AAAAAAAACgAAAAAAAAAVQ2FudE1vdmVWZXN0aW5nVG9rZW5zAAAAAAAACwAAAAAAAAARTm90RW5vdWdoQ2FwYWNpdHkAAAAAAAAMAAAAAAAAAA1Ob3RBdXRob3JpemVkAAAAAAAADQAAAAAAAAAQTmV2ZXJGdWxseVZlc3RlZAAAAA4AAAAAAAAAEVZlc3RzTW9yZVRoYW5TZW50AAAAAAAADwAAAAAAAAARSW52YWxpZEJ1cm5BbW91bnQAAAAAAAAQAAAAAAAAABFJbnZhbGlkTWludEFtb3VudAAAAAAAABEAAAAAAAAAFkludmFsaWRBbGxvd2FuY2VBbW91bnQAAAAAABIAAAAAAAAAIER1cGxpY2F0ZUluaXRpYWxCYWxhbmNlQWRkcmVzc2VzAAAAEwAAAAAAAAAKQ3VydmVFcnJvcgAAAAAAFAAAAAAAAAAQTm9XaGl0ZWxpc3RGb3VuZAAAABUAAAAAAAAAEE5vVG9rZW5JbmZvRm91bmQAAAAWAAAAAAAAAB1Ob1Zlc3RpbmdDb21wbGV4aXR5VmFsdWVGb3VuZAAAAAAAABcAAAAAAAAAEE5vQWRkcmVzc2VzVG9BZGQAAAAYAAAAAAAAABZOb0Vub3VnaHRUb2tlbnNUb1N0YXJ0AAAAAAAZAAAAAAAAABBOb3RFbm91Z2hCYWxhbmNlAAAAGgAAAAAAAAASVmVzdGluZ0JvdGhQcmVzZW50AAAAAAAbAAAAAAAAABJWZXN0aW5nTm9uZVByZXNlbnQAAAAAABwAAAAAAAAADUN1cnZlQ29uc3RhbnQAAAAAAAAdAAAAAAAAABRDdXJ2ZVNMTm90RGVjcmVhc2luZwAAAB4AAAAAAAAAEkFscmVhZHlJbml0aWFsaXplZAAAAAAAHwAAAAAAAAANQWRtaW5Ob3RGb3VuZAAAAAAAACAAAAAAAAAAEUNvbnRyYWN0TWF0aEVycm9yAAAAAAAAIQ==", + "AAAAAAAAAAAAAAANcXVlcnlfdmVyc2lvbgAAAAAAAAAAAAABAAAAEA==", + "AAAABAAAAAAAAAAAAAAADUNvbnRyYWN0RXJyb3IAAAAAAAAmAAAAAAAAABlWZXN0aW5nTm90Rm91bmRGb3JBZGRyZXNzAAAAAAACvAAAAAAAAAAdQWxsb3dhbmNlTm90Rm91bmRGb3JHaXZlblBhaXIAAAAAAAK9AAAAAAAAAA5NaW50ZXJOb3RGb3VuZAAAAAACvgAAAAAAAAAYTm9CYWxhbmNlRm91bmRGb3JBZGRyZXNzAAACvwAAAAAAAAANTm9Db25maWdGb3VuZAAAAAAAAsAAAAAAAAAADE5vQWRtaW5Gb3VuZAAAAsEAAAAAAAAADk1pc3NpbmdCYWxhbmNlAAAAAALCAAAAAAAAABhWZXN0aW5nQ29tcGxleGl0eVRvb0hpZ2gAAALDAAAAAAAAABdUb3RhbFZlc3RlZE92ZXJDYXBhY2l0eQAAAALEAAAAAAAAABVJbnZhbGlkVHJhbnNmZXJBbW91bnQAAAAAAALFAAAAAAAAABVDYW50TW92ZVZlc3RpbmdUb2tlbnMAAAAAAALGAAAAAAAAABFOb3RFbm91Z2hDYXBhY2l0eQAAAAAAAscAAAAAAAAADU5vdEF1dGhvcml6ZWQAAAAAAALIAAAAAAAAABBOZXZlckZ1bGx5VmVzdGVkAAACyQAAAAAAAAARVmVzdHNNb3JlVGhhblNlbnQAAAAAAALKAAAAAAAAABFJbnZhbGlkQnVybkFtb3VudAAAAAAAAssAAAAAAAAAEUludmFsaWRNaW50QW1vdW50AAAAAAACzAAAAAAAAAAWSW52YWxpZEFsbG93YW5jZUFtb3VudAAAAAACzQAAAAAAAAAgRHVwbGljYXRlSW5pdGlhbEJhbGFuY2VBZGRyZXNzZXMAAALOAAAAAAAAAApDdXJ2ZUVycm9yAAAAAALPAAAAAAAAABBOb1doaXRlbGlzdEZvdW5kAAAC0AAAAAAAAAAQTm9Ub2tlbkluZm9Gb3VuZAAAAtEAAAAAAAAAHU5vVmVzdGluZ0NvbXBsZXhpdHlWYWx1ZUZvdW5kAAAAAAAC0gAAAAAAAAAQTm9BZGRyZXNzZXNUb0FkZAAAAtMAAAAAAAAAFk5vRW5vdWdodFRva2Vuc1RvU3RhcnQAAAAAAtQAAAAAAAAAEE5vdEVub3VnaEJhbGFuY2UAAALVAAAAAAAAABJWZXN0aW5nQm90aFByZXNlbnQAAAAAAtYAAAAAAAAAElZlc3RpbmdOb25lUHJlc2VudAAAAAAC1wAAAAAAAAANQ3VydmVDb25zdGFudAAAAAAAAtgAAAAAAAAAFEN1cnZlU0xOb3REZWNyZWFzaW5nAAAC2QAAAAAAAAASQWxyZWFkeUluaXRpYWxpemVkAAAAAALaAAAAAAAAAA1BZG1pbk5vdEZvdW5kAAAAAAAC2wAAAAAAAAARQ29udHJhY3RNYXRoRXJyb3IAAAAAAALcAAAAAAAAAAlTYW1lQWRtaW4AAAAAAALdAAAAAAAAABROb0FkbWluQ2hhbmdlSW5QbGFjZQAAAt4AAAAAAAAAEkFkbWluQ2hhbmdlRXhwaXJlZAAAAAAC3wAAAAAAAAAQU2FtZVRva2VuQWRkcmVzcwAAAukAAAAAAAAAFEludmFsaWRNYXhDb21wbGV4aXR5AAAC6g==", + "AAAAAQAAAAAAAAAAAAAABkNvbmZpZwAAAAAAAQAAAAAAAAAOaXNfd2l0aF9taW50ZXIAAAAAAAE=", "AAAAAQAAAAAAAAAAAAAAEFZlc3RpbmdUb2tlbkluZm8AAAAEAAAAAAAAAAdhZGRyZXNzAAAAABMAAAAAAAAACGRlY2ltYWxzAAAABAAAAAAAAAAEbmFtZQAAABAAAAAAAAAABnN5bWJvbAAAAAAAEA==", "AAAAAQAAAAAAAAAAAAAAD1Zlc3RpbmdTY2hlZHVsZQAAAAACAAAAAAAAAAVjdXJ2ZQAAAAAAB9AAAAAFQ3VydmUAAAAAAAAAAAAACXJlY2lwaWVudAAAAAAAABM=", "AAAAAQAAAAAAAAAAAAAAC1Zlc3RpbmdJbmZvAAAAAAMAAAAAAAAAB2JhbGFuY2UAAAAACgAAAAAAAAAJcmVjaXBpZW50AAAAAAAAEwAAAAAAAAAIc2NoZWR1bGUAAAfQAAAABUN1cnZlAAAA", @@ -693,14 +822,14 @@ export class Client extends ContractClient { "AAAAAQAAAAAAAAAAAAAADVRva2VuSW5pdEluZm8AAAAAAAACAAAAAAAAAAd0b2tlbl9hAAAAABMAAAAAAAAAB3Rva2VuX2IAAAAAEw==", "AAAAAQAAAAAAAAAAAAAADVN0YWtlSW5pdEluZm8AAAAAAAAEAAAAAAAAAAdtYW5hZ2VyAAAAABMAAAAAAAAADm1heF9jb21wbGV4aXR5AAAAAAAEAAAAAAAAAAhtaW5fYm9uZAAAAAsAAAAAAAAACm1pbl9yZXdhcmQAAAAAAAs=", "AAAAAQAAAAAAAAAAAAAAFUxpcXVpZGl0eVBvb2xJbml0SW5mbwAAAAAAAAkAAAAAAAAABWFkbWluAAAAAAAAEwAAAAAAAAAUZGVmYXVsdF9zbGlwcGFnZV9icHMAAAAHAAAAAAAAAA1mZWVfcmVjaXBpZW50AAAAAAAAEwAAAAAAAAAYbWF4X2FsbG93ZWRfc2xpcHBhZ2VfYnBzAAAABwAAAAAAAAAWbWF4X2FsbG93ZWRfc3ByZWFkX2JwcwAAAAAABwAAAAAAAAAQbWF4X3JlZmVycmFsX2JwcwAAAAcAAAAAAAAAD3N0YWtlX2luaXRfaW5mbwAAAAfQAAAADVN0YWtlSW5pdEluZm8AAAAAAAAAAAAADHN3YXBfZmVlX2JwcwAAAAcAAAAAAAAAD3Rva2VuX2luaXRfaW5mbwAAAAfQAAAADVRva2VuSW5pdEluZm8AAAA=", + "AAAAAQAAAAAAAAAAAAAAC0FkbWluQ2hhbmdlAAAAAAIAAAAAAAAACW5ld19hZG1pbgAAAAAAABMAAAAAAAAACnRpbWVfbGltaXQAAAAAA+gAAAAG", + "AAAAAQAAAAAAAAAAAAAAD0F1dG9VbnN0YWtlSW5mbwAAAAACAAAAAAAAAAxzdGFrZV9hbW91bnQAAAALAAAAAAAAAA9zdGFrZV90aW1lc3RhbXAAAAAABg==", "AAAAAwAAAAAAAAAAAAAACFBvb2xUeXBlAAAAAgAAAAAAAAADWHlrAAAAAAAAAAAAAAAABlN0YWJsZQAAAAAAAQ==", ]), options ); } public readonly fromJSON = { - initialize: this.txFromJSON, - initialize_with_minter: this.txFromJSON, create_vesting_schedules: this.txFromJSON, claim: this.txFromJSON, burn: this.txFromJSON, @@ -712,10 +841,17 @@ export class Client extends ContractClient { query_all_vesting_info: this.txFromJSON>, query_token_info: this.txFromJSON, query_minter: this.txFromJSON, + query_config: this.txFromJSON, query_vesting_contract_balance: this.txFromJSON, query_available_to_claim: this.txFromJSON, + update_vesting_token: this.txFromJSON>, + update_max_complexity: this.txFromJSON>, update: this.txFromJSON, migrate_admin_key: this.txFromJSON>, + propose_admin: this.txFromJSON>, + revoke_admin_change: this.txFromJSON>, + accept_admin: this.txFromJSON>, add_new_key_to_storage: this.txFromJSON>, + query_version: this.txFromJSON, }; } diff --git a/packages/core/app/earn/page.tsx b/packages/core/app/earn/page.tsx new file mode 100644 index 00000000..23cf55f6 --- /dev/null +++ b/packages/core/app/earn/page.tsx @@ -0,0 +1,554 @@ +"use client"; +import { + Tab, + Tabs, + Typography, + useMediaQuery, + useTheme, + Button, +} from "@mui/material"; +import { Box } from "@mui/system"; +import { useAppStore, usePersistStore } from "@phoenix-protocol/state"; +import { + StrategiesTable, + YieldSummary, + BondModal, + UnbondModal, + ClaimAllModal, +} from "@phoenix-protocol/ui"; + +// Fix the imports to properly import StrategyRegistry +import { Strategy, StrategyMetadata } from "@phoenix-protocol/strategies"; +import { StrategyRegistry } from "@phoenix-protocol/strategies"; + +import { motion, AnimatePresence } from "framer-motion"; +import { useEffect, useState, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import { useContractTransaction } from "../../hooks/useContractTransaction"; + +// Define the Token type +interface Token { + name: string; + icon: string; + usdValue: number; + amount: number; + category: string; +} + +export default function EarnPage(): JSX.Element { + const router = useRouter(); + const appStore = useAppStore(); + const persistStore = usePersistStore(); + const walletAddress = persistStore.wallet.address; + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const { executeContractTransaction } = useContractTransaction(); + + // Strategy state + const [allStrategies, setAllStrategies] = useState< + { strategy: Strategy; metadata: StrategyMetadata }[] + >([]); + const [userStrategies, setUserStrategies] = useState< + { strategy: Strategy; metadata: StrategyMetadata }[] + >([]); + const [totalValue, setTotalValue] = useState(0); + const [claimableRewards, setClaimableRewards] = useState(0); + const [isLoading, setIsLoading] = useState(true); + + // Modal States + const [bondModalOpen, setBondModalOpen] = useState(false); + const [unbondModalOpen, setUnbondModalOpen] = useState(false); + const [claimAllModalOpen, setClaimAllModalOpen] = useState(false); + const [selectedStrategy, setSelectedStrategy] = + useState(null); + + // Filters + const [tabValue, setTabValue] = useState(0); + + // Initialize strategies on first load + useEffect(() => { + // Don't register mock providers anymore - we're using real ones + // registerMockProviders(); + + appStore.setLoading(true); + loadStrategies(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Load user-specific data when wallet changes + useEffect(() => { + if (walletAddress) { + loadUserStrategies(); + calculateUserStats(); + } else { + setUserStrategies([]); + setTotalValue(0); + setClaimableRewards(0); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [walletAddress, allStrategies]); + + // Set loading spinner timeout + useEffect(() => { + const timer = setTimeout(() => { + appStore.setLoading(false); + }, 1000); + return () => clearTimeout(timer); + }, [appStore]); + + // Load all strategies with their metadata + const loadStrategies = useCallback(async () => { + setIsLoading(true); + try { + console.log("Getting strategies from StrategyRegistry..."); + console.log("StrategyRegistry available?", !!StrategyRegistry); + + const providers = StrategyRegistry.getProviders(); + console.log("Available providers:", providers.length); + providers.forEach((p) => console.log("- Provider:", p.id)); + + const strategies = await StrategyRegistry.getAllStrategies(); + console.log("Strategies found:", strategies.length); + + const strategiesWithMetadata = await Promise.all( + strategies.map(async (strategy) => { + const metadata = await StrategyRegistry.getStrategyMetadata(strategy); + // Fetch user data immediately if wallet is connected + let userStake = 0; + let userRewards = 0; + let hasJoined = false; + if (walletAddress) { + try { + userStake = await strategy.getUserStake(walletAddress); + userRewards = await strategy.getUserRewards(walletAddress); + hasJoined = await strategy.hasUserJoined(walletAddress); + } catch (e) { + console.error( + "Error fetching user data for strategy", + metadata.id, + e + ); + } + } + return { + strategy, + metadata: { ...metadata, userStake, userRewards, hasJoined }, + }; + }) + ); + setAllStrategies(strategiesWithMetadata); + } catch (error) { + console.error("Error loading strategies:", error); + } finally { + setIsLoading(false); + } + }, [walletAddress]); + + // Filter out strategies the user has joined + const loadUserStrategies = useCallback(async () => { + if (!walletAddress) return; + const joined = allStrategies.filter((s) => s.metadata.hasJoined); + setUserStrategies(joined); + }, [walletAddress, allStrategies]); + + // Calculate total value and rewards from user strategies + const calculateUserStats = useCallback(async () => { + if (!walletAddress) return; + let totalStaked = 0; + let totalRewards = 0; + allStrategies.forEach(({ metadata }) => { + totalStaked += metadata.userStake || 0; + totalRewards += metadata.userRewards || 0; + }); + setTotalValue(totalStaked); + setClaimableRewards(totalRewards); + }, [walletAddress, allStrategies]); + + // Modal Handlers + const handleBondClick = useCallback((strategyMeta: StrategyMetadata) => { + setSelectedStrategy(strategyMeta); + setBondModalOpen(true); + }, []); + + const handleUnbondClick = useCallback( + (strategyMeta: StrategyMetadata) => { + if (!walletAddress) return; + setSelectedStrategy(strategyMeta); + setUnbondModalOpen(true); + }, + [walletAddress] + ); + + const handleClaimAllClick = useCallback(() => { + if (!walletAddress || claimableRewards <= 0) return; + setClaimAllModalOpen(true); + }, [walletAddress, claimableRewards]); + + const handleCloseModals = () => { + setBondModalOpen(false); + setUnbondModalOpen(false); + setClaimAllModalOpen(false); + setSelectedStrategy(null); + }; + + // Transaction Execution Handlers + const handleConfirmBond = useCallback( + async (tokenAmounts: { token: Token; amount: number }[]) => { + if (!selectedStrategy || !walletAddress) return; + + const strategyInstance = allStrategies.find( + (s) => s.metadata.id === selectedStrategy.id + )?.strategy; + + if (!strategyInstance) { + console.error("Strategy instance not found for bonding."); + // Potentially show a toast error to the user + return; + } + + const amountA = tokenAmounts[0]?.amount; + const amountB = + tokenAmounts.length > 1 ? tokenAmounts[1]?.amount : undefined; + + if (amountA === undefined) { + console.error("Amount A is undefined for bonding"); + // Potentially show a toast error + return; + } + // Note: The strategy's bond method should internally check if amountB is required. + + await executeContractTransaction({ + contractType: selectedStrategy.contractType, // This is indicative; strategy uses its own client + contractAddress: selectedStrategy.contractAddress, // This is indicative + transactionFunction: async (_client, _restore) => { + // _client and _restore from useContractTransaction are ignored here. + // The strategyInstance.bond method now returns the AssembledTransaction. + return strategyInstance.bond(walletAddress!, amountA, amountB); + }, + options: { + onSuccess: () => { + loadStrategies(); // Refresh data on success + handleCloseModals(); + }, + }, + }); + }, + [ + selectedStrategy, + walletAddress, + executeContractTransaction, + allStrategies, + loadStrategies, + // handleCloseModals, // Added if it's stable or include its dependencies + ] + ); + + const handleConfirmUnbond = useCallback( + async (params: number | { lpAmount: bigint; timestamp: bigint }) => { + if (!selectedStrategy || !walletAddress) return; + + const strategyInstance = allStrategies.find( + (s) => s.metadata.id === selectedStrategy.id + )?.strategy; + + if (!strategyInstance) { + console.error("Strategy instance not found for unbonding."); + return; + } + + await executeContractTransaction({ + contractType: selectedStrategy.contractType, // Indicative + contractAddress: selectedStrategy.contractAddress, // Indicative + transactionFunction: async (_client, _restore) => { + return strategyInstance.unbond(walletAddress!, params); + }, + options: { + onSuccess: () => { + loadStrategies(); + handleCloseModals(); + }, + }, + }); + }, + [ + selectedStrategy, + walletAddress, + executeContractTransaction, + allStrategies, + loadStrategies, + // handleCloseModals, // Added if it's stable or include its dependencies + ] + ); + + const handleClaimStrategy = useCallback( + (strategy: Strategy, metadata: StrategyMetadata): Promise => { + return new Promise((resolve, reject) => { + if (!walletAddress) { + console.warn( + "Claim attempt with no wallet address for strategy:", + metadata.id + ); + reject(new Error("Wallet not connected. Cannot claim.")); + return; + } + + executeContractTransaction({ + contractType: metadata.contractType, + contractAddress: metadata.contractAddress, + transactionFunction: (_client, _restore) => { + // Ensure walletAddress is not null here, though checked above. + // The strategy.claim method itself should handle Soroban client interactions. + return strategy.claim(walletAddress!); + }, + options: { + onSuccess: () => { + console.log( + `Claim successful for ${metadata.name}. Refreshing strategies.` + ); + loadStrategies(); // Refresh data on success + resolve(); // Resolve the promise that ClaimAllModal is awaiting + }, + // Errors from executeContractTransaction (e.g. user rejection, network issues) + // will be caught by the .catch() below. + }, + }).catch((error) => { + // This catches rejections from the promise returned by executeContractTransaction. + console.error(`Claim failed for ${metadata.name}:`, error); + reject(error); // Reject the promise that ClaimAllModal is awaiting + }); + }); + }, + [walletAddress, executeContractTransaction, loadStrategies] + ); + + // Tab handling + const handleChange = (event: React.SyntheticEvent, newValue: number) => { + setTabValue(newValue); + }; + + const handleViewStrategyDetails = useCallback( + (strategyId: string) => { + router.push(`/earn/${strategyId}`); + }, + [router] + ); + + // Prepare data for UI components + const userStrategyIds = userStrategies.map((s) => s.metadata.id); + const discoverableRawStrategies = allStrategies.filter( + (s) => !userStrategyIds.includes(s.metadata.id) + ); + + const discoverStrategiesUI = discoverableRawStrategies.map((s) => ({ + ...s.metadata, + isMobile, + })); + + const userStrategiesUI = userStrategies.map((s) => ({ + ...s.metadata, + isMobile, + })); + + // Prepare claimable strategies for modal + const claimableForModal = allStrategies + .filter( + (s) => + s.metadata.hasJoined && + s.metadata.userRewards && + s.metadata.userRewards > 0 + ) + .map((s) => ({ + strategy: s.strategy, + metadata: s.metadata, + rewards: s.metadata.userRewards || 0, + })); + + return ( + + + + Earn + + + + + {/* Tabs */} + + + + + + + + + {tabValue === 0 ? ( + walletAddress && + !isLoading && + discoverStrategiesUI.length === 0 && + userStrategies.length > 0 ? ( + + + You've explored all available strategies! + + + All strategies are currently part of 'Your + Strategies'. + + + + ) : ( + + ) + ) : ( + + )} + + + + + {/* Modals */} + + + { + handleCloseModals(); + loadStrategies(); + }} + claimableStrategies={claimableForModal} + onClaimStrategy={handleClaimStrategy} + /> + + ); +} diff --git a/packages/core/app/help-center/category/[categoryID]/page.tsx b/packages/core/app/help-center/category/[categoryID]/page.tsx index 0ceafb72..b4356ac7 100644 --- a/packages/core/app/help-center/category/[categoryID]/page.tsx +++ b/packages/core/app/help-center/category/[categoryID]/page.tsx @@ -29,6 +29,7 @@ export default function Page(props: CategoryPageProps) { useEffect(() => { init(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const init = async () => { diff --git a/packages/core/app/help-center/page.tsx b/packages/core/app/help-center/page.tsx index 53698389..6e49b9a2 100644 --- a/packages/core/app/help-center/page.tsx +++ b/packages/core/app/help-center/page.tsx @@ -69,6 +69,7 @@ export default function Page() { useEffect(() => { init(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const init = async () => { diff --git a/packages/core/app/history/page.tsx b/packages/core/app/history/page.tsx index 05e620f2..b74366fa 100644 --- a/packages/core/app/history/page.tsx +++ b/packages/core/app/history/page.tsx @@ -348,7 +348,7 @@ export default function Page() { > Transaction History - + - + {historicalPrices.length > 0 && ( - + + + )} @@ -376,30 +384,34 @@ export default function Page() { totalTrades={meta.totalTrades} mostTradedAsset={meta.mostTradedAsset} /> - {!historyLoading ? ( - { - setHistory([]); - setActiveView(view); - }} - loggedIn={!!appStorePersist.wallet.address} - activeFilters={activeFilters} - applyFilters={(newFilters: ActiveFilters) => applyFilters(newFilters)} - handleSort={(column) => - handleSortChange( - column as any, - sortOrder === "asc" ? "desc" : "asc" - ) - } - /> - ) : ( - - )} + + {!historyLoading ? ( + { + setHistory([]); + setActiveView(view); + }} + loggedIn={!!appStorePersist.wallet.address} + activeFilters={activeFilters} + applyFilters={(newFilters: ActiveFilters) => + applyFilters(newFilters) + } + handleSort={(column) => + handleSortChange( + column as any, + sortOrder === "asc" ? "desc" : "asc" + ) + } + /> + ) : ( + + )} + + + + + - @@ -466,7 +756,7 @@ export default function Page() { - window.open("https://app.kado.money")} /> + + window.open("https://app.kado.money")} + /> + {/* Asset Info Modal */} - {selectedTokenForInfo && ( - setTokenInfoOpen(false)} - asset={selectedTokenForInfo} - /> - )} + + { + setTokenInfoOpen(false); + if (loadingAssetInfo) { + setLoadingAssetInfo(false); + } + }} + asset={selectedTokenForInfo || emptyAssetPlaceholder} + userBalance={0} + pools={selectedAssetPools} + // @ts-ignore + tradingVolume7d={tradingVolume7d} + loading={loadingAssetInfo} + /> {/* Vesting Modal */} {vestingInfo.length > 0 && ( diff --git a/packages/core/app/pools/[poolAddress]/page.tsx b/packages/core/app/pools/[poolAddress]/page.tsx index fbe6a055..b665dd9e 100644 --- a/packages/core/app/pools/[poolAddress]/page.tsx +++ b/packages/core/app/pools/[poolAddress]/page.tsx @@ -1,6 +1,7 @@ +/* eslint-disable react-hooks/exhaustive-deps */ "use client"; -import React, { useEffect, useState, use } from "react"; +import React, { useEffect, useState, use, useMemo, useCallback } from "react"; import * as refuse from "react-usestateref"; import { Box, GlobalStyles, Grid, Skeleton, Typography } from "@mui/material"; import { @@ -111,14 +112,133 @@ export default function Page(props: PoolPageProps) { const [unstakeAmount, setUnstakeAmount] = useState(0); const [unstakeTimestamp, setUnstakeTimestamp] = useState(0); - const PairContract = new PhoenixPairContract.Client({ - contractId: params.poolAddress, - networkPassphrase: constants.NETWORK_PASSPHRASE, - rpcUrl: constants.RPC_URL, - }); + // Memoize the PairContract + const PairContract = useMemo( + () => + new PhoenixPairContract.Client({ + contractId: params.poolAddress, + networkPassphrase: constants.NETWORK_PASSPHRASE, + rpcUrl: constants.RPC_URL, + }), + [params.poolAddress] + ); + const appStore = useAppStore(); - const fetchStakingAddress = async (): Promise => { + const loadRewards = useCallback( + async (stakeContract = StakeContract) => { + try { + // Stake Contract + const _rewards = await stakeContract?.query_withdrawable_rewards({ + user: storePersist.wallet.address!, + }); + + const __rewards = _rewards?.result.rewards?.map(async (reward: any) => { + // Get the token + const token = await store.fetchTokenInfo(reward.reward_address); + return { + name: token?.symbol.toUpperCase(), + icon: `/cryptoIcons/${token?.symbol.toLowerCase()}.svg`, + usdValue: "0", + amount: + Number(reward.reward_amount.toString()) / 10 ** token?.decimals!, + category: "", + }; + }); + + const rew = await Promise.all(__rewards); + setRewards(rew); + } catch (e) { + console.log(e); + } + }, + [StakeContract, store, storePersist.wallet.address] + ); + + const fetchStakes = useCallback( + async ( + name = lpToken?.name, + stakeContract = StakeContract, + calcApr = maxApr, + tokenPrice = lpTokenPrice + ) => { + if (storePersist.wallet.address) { + // Get user stakes + const stakesA = await stakeContract?.query_staked( + { + address: storePersist.wallet.address!, + }, + { simulate: false } + ); + + const stakes = await stakesA.simulate({ restore: true }); + // If stakes are okay + if (stakes?.result) { + // If filled + if (stakes.result.stakes.length > 0) { + //@ts-ignore + const _stakes: Entry[] = stakes.result.stakes.map((stake: any) => { + return { + icon: `/cryptoIcons/poolIcon.png`, + title: name!, + apr: + // Calculate APR + + ( + (time.daysSinceTimestamp(Number(stake.stake_timestamp)) > 60 + ? 60 + : time.daysSinceTimestamp( + Number(stake.stake_timestamp) + )) * + (calcApr / 2 / 60) + ).toFixed(2) + "%", + lockedPeriod: + time.daysSinceTimestamp(Number(stake.stake_timestamp)) + + " days", + amount: { + tokenAmount: Number(stake.stake) / 10 ** 7, + tokenValueInUsd: ( + (Number(stake.stake) / 10 ** 7) * + tokenPrice + ).toFixed(2), + }, + onClick: () => { + setIsFixUnstake(false); + setUnstakeAmount(Number(stake.stake) / 10 ** 7); + setUnstakeTimestamp(stake.stake_timestamp); + setUnstakeModalOpen(true); + }, + onClickFix: () => { + setIsFixUnstake(true); + setUnstakeAmount(Number(stake.stake) / 10 ** 7); + setUnstakeTimestamp(stake.stake_timestamp); + setUnstakeModalOpen(true); + }, + }; + }); + setUserStakes(_stakes); + await loadRewards(stakeContract); + return _stakes; + } + } + } + }, + [ + lpToken?.name, + StakeContract, + maxApr, + lpTokenPrice, + storePersist.wallet.address, + loadRewards, + ] + ); + + /** + * Fetch staking address with error handling + */ + const fetchStakingAddress = useCallback(async (): Promise< + string | undefined + > => { try { // Fetch pool config and info from chain const [pairConfig, pairInfo] = await Promise.all([ @@ -126,155 +246,181 @@ export default function Page(props: PoolPageProps) { PairContract.query_pool_info(), ]); - console.log(pairConfig.result); - console.log("hi"); // When results ok... if (pairConfig?.result && pairInfo?.result) { - // Fetch token infos from chain and save in global appstore - const [_tokenA, _tokenB, _lpToken, stakeContractAddress] = - await Promise.all([ - store.fetchTokenInfo(pairConfig.result.token_a), - store.fetchTokenInfo(pairConfig.result.token_b), - store.fetchTokenInfo(pairConfig.result.share_token, true), - new PhoenixStakeContract.Client({ - contractId: pairConfig.result.stake_contract.toString(), - networkPassphrase: constants.NETWORK_PASSPHRASE, - rpcUrl: constants.RPC_URL, - publicKey: storePersist.wallet.address, - signTransaction: (tx: string) => new Signer().sign(tx), - }), - ]); - return pairConfig.result.stake_contract.toString(); + // Fetch token infos + const stakeContractId = pairConfig.result.stake_contract.toString(); + const stakeContractAddress = new PhoenixStakeContract.Client({ + contractId: stakeContractId, + networkPassphrase: constants.NETWORK_PASSPHRASE, + rpcUrl: constants.RPC_URL, + publicKey: storePersist.wallet.address, + signTransaction: (tx: string) => new Signer().sign(tx), + }); + + return stakeContractId; } } catch (e) { - console.log(e); + console.log("Error fetching staking address:", e); } - }; - // Provide Liquidity - const provideLiquidity = async ( - tokenAAmount: number, - tokenBAmount: number - ) => { - await executeContractTransaction({ - contractType: "pair", - contractAddress: params.poolAddress, - transactionFunction: async (client, restore) => { - return client.provide_liquidity( - { - sender: storePersist.wallet.address!, - desired_a: BigInt( - (tokenAAmount * 10 ** (tokenA?.decimals || 7)).toFixed(0) - ), - desired_b: BigInt( - (tokenBAmount * 10 ** (tokenB?.decimals || 7)).toFixed(0) - ), - min_a: undefined, - min_b: undefined, - custom_slippage_bps: undefined, - deadline: undefined, - }, - { simulate: !restore } - ); - }, - }); - // Refresh pool data + return undefined; + }, [PairContract, storePersist.wallet.address]); + + /** + * Refresh pool data and balances after operations + */ + const refreshPoolData = useCallback(async () => { await getPool(); - setTokenAmounts([tokenAAmount, tokenBAmount]); - setTimeout(() => { - getPool(); - }, 7000); - }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + /** + * Provide liquidity to the pool with callback for balance updates + */ + const provideLiquidity = useCallback( + async (tokenAAmount: number, tokenBAmount: number) => { + await executeContractTransaction({ + contractType: "pair", + contractAddress: params.poolAddress, + transactionFunction: async (client, restore) => { + return client.provide_liquidity( + { + sender: storePersist.wallet.address!, + desired_a: BigInt( + (tokenAAmount * 10 ** (tokenA?.decimals || 7)).toFixed(0) + ), + desired_b: BigInt( + (tokenBAmount * 10 ** (tokenB?.decimals || 7)).toFixed(0) + ), + min_a: undefined, + min_b: undefined, + custom_slippage_bps: undefined, + deadline: undefined, + }, + { simulate: !restore } + ); + }, + }); + }, + [ + executeContractTransaction, + params.poolAddress, + storePersist.wallet.address, + tokenA?.decimals, + tokenB?.decimals, + refreshPoolData, + ] + ); - // Remove Liquidity - const removeLiquidity = async (lpTokenAmount: number, fix?: boolean) => { - await executeContractTransaction({ - contractType: "pair", - contractAddress: params.poolAddress, - transactionFunction: async (client, restore) => { - return client.withdraw_liquidity( - { - sender: storePersist.wallet.address!, - share_amount: BigInt( - (lpTokenAmount * 10 ** (lpToken?.decimals || 7)).toFixed(0) - ), - min_a: BigInt(1), - min_b: BigInt(1), - deadline: undefined, - }, - { simulate: !restore } - ); - }, - }); - setTokenAmounts([lpTokenAmount]); - // Wait 7 Seconds for the next block and fetch new balances - setTimeout(() => { - getPool(); - }, 7000); - }; + /** + * Remove liquidity from the pool with callback for balance updates + */ + const removeLiquidity = useCallback( + async (lpTokenAmount: number, fix?: boolean) => { + await executeContractTransaction({ + contractType: "pair", + contractAddress: params.poolAddress, + transactionFunction: async (client, restore) => { + return client.withdraw_liquidity( + { + sender: storePersist.wallet.address!, + share_amount: BigInt( + (lpTokenAmount * 10 ** (lpToken?.decimals || 7)).toFixed(0) + ), + min_a: BigInt(1), + min_b: BigInt(1), + deadline: undefined, + }, + { simulate: !restore } + ); + }, + }); + }, + [ + executeContractTransaction, + params.poolAddress, + storePersist.wallet.address, + lpToken?.decimals, + refreshPoolData, + ] + ); - // Stake - const stake = async (lpTokenAmount: number) => { - let stakeAddress: string | undefined = stakeContractAddress; - if (stakeContractAddress === "") { - stakeAddress = await fetchStakingAddress(); - } + /** + * Stake LP tokens with callback for balance updates + */ + const stake = useCallback( + async (lpTokenAmount: number) => { + let stakeAddress: string | undefined = stakeContractAddress; + if (!stakeAddress) { + stakeAddress = await fetchStakingAddress(); + if (!stakeAddress) return; + } - await executeContractTransaction({ - contractType: "stake", - contractAddress: stakeAddress!, - transactionFunction: async (client, restore) => { - return client.bond( - { - sender: storePersist.wallet.address!, - tokens: BigInt( - (lpTokenAmount * 10 ** (lpToken?.decimals || 7)).toFixed(0) - ), - }, - { simulate: !restore } - ); - }, - }); - await fetchStakes(); - setTokenAmounts([lpTokenAmount]); - // Wait 7 Seconds for the next block and fetch new balances - setTimeout(() => { - getPool(); - }, 7000); - }; + await executeContractTransaction({ + contractType: "stake", + contractAddress: stakeAddress, + transactionFunction: async (client, restore) => { + return client.bond( + { + sender: storePersist.wallet.address!, + tokens: BigInt( + (lpTokenAmount * 10 ** (lpToken?.decimals || 7)).toFixed(0) + ), + }, + { simulate: !restore } + ); + }, + }); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + stakeContractAddress, + fetchStakingAddress, + executeContractTransaction, + storePersist.wallet.address, + lpToken?.decimals, + fetchStakes, + refreshPoolData, + ] + ); - // Stake - const unstake = async ( - lpTokenAmount: number, - stake_timestamp: number, - fix?: boolean - ) => { - let stakeAddress: string | undefined = stakeContractAddress; - if (stakeContractAddress === "") { - stakeAddress = await fetchStakingAddress(); - } + /** + * Unstake LP tokens with callback for balance updates + */ + const unstake = useCallback( + async (lpTokenAmount: number, stake_timestamp: number, fix?: boolean) => { + let stakeAddress: string | undefined = stakeContractAddress; + if (!stakeAddress) { + stakeAddress = await fetchStakingAddress(); + if (!stakeAddress) return; + } - await executeContractTransaction({ - contractType: "stake", - contractAddress: stakeAddress!, - transactionFunction: async (client, restore) => { - return client.unbond( - { - sender: storePersist.wallet.address!, - stake_amount: BigInt( - (lpTokenAmount * 10 ** (lpToken?.decimals || 7)).toFixed(0) - ), - stake_timestamp: BigInt(stake_timestamp), - }, - { simulate: !restore } - ); - }, - }); - setTokenAmounts([lpTokenAmount]); - // Wait 7 Seconds for the next block and fetch new balances - setTimeout(() => { - getPool(); - }, 7000); - }; + await executeContractTransaction({ + contractType: "stake", + contractAddress: stakeAddress, + transactionFunction: async (client, restore) => { + return client.unbond( + { + sender: storePersist.wallet.address!, + stake_amount: BigInt( + (lpTokenAmount * 10 ** (lpToken?.decimals || 7)).toFixed(0) + ), + stake_timestamp: BigInt(stake_timestamp), + }, + { simulate: !restore } + ); + }, + }); + }, + [ + stakeContractAddress, + fetchStakingAddress, + executeContractTransaction, + storePersist.wallet.address, + lpToken?.decimals, + refreshPoolData, + ] + ); // Function to fetch pool config and info from chain const getPool = async () => { @@ -285,8 +431,6 @@ export default function Page(props: PoolPageProps) { PairContract.query_pool_info(), ]); - console.log(pairConfig.result); - // When results ok... if (pairConfig?.result && pairInfo?.result) { // Fetch token infos from chain and save in global appstore @@ -303,7 +447,6 @@ export default function Page(props: PoolPageProps) { publicKey: storePersist.wallet.address, }), ]); - console.log(_lpToken); setStakeContractAddress(pairConfig.result.stake_contract.toString()); // Fetch prices and calculate TVL @@ -449,108 +592,19 @@ export default function Page(props: PoolPageProps) { } }; - const fetchStakes = async ( - name = lpToken?.name, - stakeContract = StakeContract, - calcApr = maxApr, - tokenPrice = lpTokenPrice - ) => { - if (storePersist.wallet.address) { - // Get user stakes - const stakesA = await stakeContract?.query_staked( - { - address: storePersist.wallet.address!, - }, - { simulate: false } - ); - - const stakes = await stakesA.simulate({ restore: true }); - // If stakes are okay - if (stakes?.result) { - // If filled - if (stakes.result.stakes.length > 0) { - //@ts-ignore - const _stakes: Entry[] = stakes.result.stakes.map((stake: any) => { - return { - icon: `/cryptoIcons/poolIcon.png`, - title: name!, - apr: - // Calculate APR - - ( - (time.daysSinceTimestamp(Number(stake.stake_timestamp)) > 60 - ? 60 - : time.daysSinceTimestamp(Number(stake.stake_timestamp))) * - (calcApr / 2 / 60) - ).toFixed(2) + "%", - lockedPeriod: - time.daysSinceTimestamp(Number(stake.stake_timestamp)) + - " days", - amount: { - tokenAmount: Number(stake.stake) / 10 ** 7, - tokenValueInUsd: ( - (Number(stake.stake) / 10 ** 7) * - tokenPrice - ).toFixed(2), - }, - onClick: () => { - setIsFixUnstake(false); - setUnstakeAmount(Number(stake.stake) / 10 ** 7); - setUnstakeTimestamp(stake.stake_timestamp); - setUnstakeModalOpen(true); - }, - onClickFix: () => { - setIsFixUnstake(true); - setUnstakeAmount(Number(stake.stake) / 10 ** 7); - setUnstakeTimestamp(stake.stake_timestamp); - setUnstakeModalOpen(true); - }, - }; - }); - setUserStakes(_stakes); - await loadRewards(stakeContract); - return _stakes; - } - } - } - }; - - const loadRewards = async (stakeContract = StakeContract) => { - try { - // Stake Contract - const _rewards = await stakeContract?.query_withdrawable_rewards({ - user: storePersist.wallet.address!, - }); - - const __rewards = _rewards?.result.rewards?.map(async (reward: any) => { - // Get the token - const token = await store.fetchTokenInfo(reward.reward_address); - return { - name: token?.symbol.toUpperCase(), - icon: `/cryptoIcons/${token?.symbol.toLowerCase()}.svg`, - usdValue: "0", - amount: - Number(reward.reward_amount.toString()) / 10 ** token?.decimals!, - category: "", - }; - }); - - const rew = await Promise.all(__rewards); - setRewards(rew); - } catch (e) { - console.log(e); - } - }; - - const claimTokens = async () => { + /** + * Claim rewards with callback for balance updates + */ + const claimTokens = useCallback(async () => { let stakeAddress: string | undefined = stakeContractAddress; - if (stakeContractAddress === "") { + if (!stakeAddress) { stakeAddress = await fetchStakingAddress(); + if (!stakeAddress) return; } await executeContractTransaction({ contractType: "stake", - contractAddress: stakeAddress!, + contractAddress: stakeAddress, transactionFunction: async (client, restore) => { return client.withdraw_rewards( { @@ -560,15 +614,19 @@ export default function Page(props: PoolPageProps) { ); }, }); - // Wait 7 Seconds for the next block and fetch new balances - setTimeout(() => { - getPool(); - }, 7000); - }; - + }, [ + stakeContractAddress, + fetchStakingAddress, + executeContractTransaction, + storePersist.wallet.address, + refreshPoolData, + ]); + + // Fetch pool data when address changes useEffect(() => { - getPool(); - // eslint-disable-next-line react-hooks/exhaustive-deps + if (storePersist.wallet.address) { + getPool(); + } }, [storePersist.wallet.address]); if (!params.poolAddress || poolNotFound) { diff --git a/packages/core/app/style.css b/packages/core/app/style.css deleted file mode 100644 index cd16ed24..00000000 --- a/packages/core/app/style.css +++ /dev/null @@ -1,3 +0,0 @@ -body { - background: linear-gradient(180deg, #1f2123 0%, #131517 100%); -} diff --git a/packages/core/app/swap/page.tsx b/packages/core/app/swap/page.tsx index 2d0b27b3..2e838394 100644 --- a/packages/core/app/swap/page.tsx +++ b/packages/core/app/swap/page.tsx @@ -7,6 +7,7 @@ import React, { useState, useCallback, useMemo, + useRef, type JSX, } from "react"; import { useDebounce } from "use-debounce"; @@ -40,6 +41,7 @@ import { } from "@phoenix-protocol/utils"; import { LoadingSwap, SwapError, SwapSuccess } from "@/components/Modal/Modal"; import { Box } from "@mui/material"; +import ClientOnly from "@/providers/ClientOnlyProvider"; /** * SwapPage Component @@ -49,6 +51,9 @@ import { Box } from "@mui/material"; * @component */ export default function SwapPage(): JSX.Element { + // State for client-side rendering detection + const [isClient, setIsClient] = useState(false); + // State variables declaration and initialization const [optionsOpen, setOptionsOpen] = useState(false); const [assetSelectorOpen, setAssetSelectorOpen] = useState(false); @@ -80,15 +85,97 @@ export default function SwapPage(): JSX.Element { const storePersist = usePersistStore(); const appStore = useAppStore(); + // Flags to prevent multiple initializations and rerenders + const initialSetupComplete = useRef(false); + const operationsUpdateComplete = useRef(false); + const hasTokensLoaded = useRef(false); + const hasPoolsLoaded = useRef(false); + const prevOperations = useRef(""); + const simulationRequestRef = useRef(null); + const tokenLoadCount = useRef(0); + const [fromAmount] = useDebounce(tokenAmounts[0], 500); const { executeContractTransaction } = useContractTransaction(); + // Mark component as client-side rendered on mount + useEffect(() => { + setIsClient(true); + }, []); + + /** + * Fetches token balances after a transaction completes + */ + const refreshTokenBalances = useCallback(async () => { + if (fromToken?.name) { + await appStore.fetchTokenInfo(fromToken.name); + } + if (toToken?.name) { + await appStore.fetchTokenInfo(toToken.name); + } + }, [appStore, fromToken?.name, toToken?.name]); + + /** + * Handles adding a trustline for a token. + */ + const handleTrustLine = useCallback( + async (tokenAddress: string): Promise => { + if (!storePersist.wallet.address || !tokenAddress) return; + + try { + const trust = await checkTrustline( + storePersist.wallet.address, + tokenAddress + ); + + setTrustlineButtonActive(!trust.exists); + setTrustlineTokenSymbol(trust.asset?.code || ""); + + // Only fetch trustline asset if needed + if (!trust.exists) { + const tlAsset = await appStore.fetchTokenInfo( + "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA" + ); + if (tlAsset?.decimals) { + setTrustlineAssetAmount( + Number(tlAsset.balance) / 10 ** tlAsset.decimals + ); + } + } + + setTrustlineTokenName(trust.asset?.contract || ""); + } catch (error) { + console.error("Error checking trustline:", error); + } + }, + [storePersist.wallet.address, appStore] + ); + + /** + * Adds a trustline for the specified token. + */ + const addTrustLine = useCallback(async (): Promise => { + if (!storePersist.wallet.address || !trustlineTokenName) return; + + try { + setTxBroadcasting(true); + await fetchAndIssueTrustline( + storePersist.wallet.address, + trustlineTokenName + ); + setTrustlineButtonActive(false); + + // Refresh token balances after creating trustline + setTimeout(refreshTokenBalances, 7000); + } catch (e) { + console.log("Error creating trustline:", e); + } finally { + setTxBroadcasting(false); + } + }, [storePersist.wallet.address, trustlineTokenName, refreshTokenBalances]); + /** * Executes the swap transaction. - * This function signs and sends the transaction using WalletConnect or Signer. - * - * @async */ const doSwap = useCallback(async (): Promise => { try { @@ -110,256 +197,437 @@ export default function SwapPage(): JSX.Element { { simulate: !restore } ); }, + options: { + onSuccess: () => { + setTimeout(refreshTokenBalances, 7000); + }, + }, }); - - // Wait for the next block and fetch token balances - setTimeout(async () => { - await appStore.fetchTokenInfo(fromToken?.name!); - await appStore.fetchTokenInfo(toToken?.name!); - }, 7000); } catch (error) { console.log("Error during swap transaction", error); } }, [ - appStore, - fromToken?.name, maxSpread, operations, - storePersist, - toToken?.name, + storePersist.wallet.address, tokenAmounts, executeContractTransaction, + refreshTokenBalances, ]); /** - * Simulates the swap transaction to determine the exchange rate and network fee. - * - * @async + * Simulates the swap transaction with throttling to prevent excessive API calls */ const doSimulateSwap = useCallback(async (): Promise => { - if (fromToken && toToken) { - if (tokenAmounts[0] === 0) { - setTokenAmounts([0, 0]); - setExchangeRate(""); - setNetworkFee(""); - return; - } + // Clear any pending simulation requests + if (simulationRequestRef.current) { + clearTimeout(simulationRequestRef.current); + simulationRequestRef.current = null; + } - setLoadingSimulate(true); - try { - const contract = new PhoenixMultihopContract.Client({ - contractId: constants.MULTIHOP_ADDRESS, - networkPassphrase: constants.NETWORK_PASSPHRASE, - rpcUrl: constants.RPC_URL, - }); + // Guard clauses to prevent unnecessary simulation + if (!fromToken || !toToken || !operations.length || tokenAmounts[0] === 0) { + setTokenAmounts((prevAmounts) => [prevAmounts[0], 0]); + setExchangeRate(""); + setNetworkFee(""); + return; + } - const tx = await contract.simulate_swap({ - operations, - amount: BigInt(tokenAmounts[0] * 10 ** 7), - pool_type: 0, - }); + // Prevent simulation if already simulating + if (loadingSimulate) return; - if (tx.result.ask_amount && tx.result.commission_amounts) { - const _exchangeRate = - (Number(tx.result.ask_amount) - - Number(tx.result.commission_amounts[0][1])) / - Number(tokenAmounts[0]); + setLoadingSimulate(true); + try { + const contract = new PhoenixMultihopContract.Client({ + contractId: constants.MULTIHOP_ADDRESS, + networkPassphrase: constants.NETWORK_PASSPHRASE, + rpcUrl: constants.RPC_URL, + }); - setExchangeRate( - `${(_exchangeRate / 10 ** 7).toFixed(2)} ${toToken?.name} per ${ - fromToken?.name - }` - ); - setNetworkFee( - `${Number(tx.result.commission_amounts[0][1]) / 10 ** 7} ${ - fromToken?.name - }` - ); + const tx = await contract.simulate_swap({ + operations, + amount: BigInt(tokenAmounts[0] * 10 ** 7), + pool_type: 0, + }); + + if (tx.result.ask_amount && tx.result.commission_amounts) { + const _exchangeRate = + (Number(tx.result.ask_amount) - + Number(tx.result.commission_amounts[0][1])) / + Number(tokenAmounts[0]); + + setExchangeRate( + `${(_exchangeRate / 10 ** 7).toFixed(2)} ${toToken?.name} per ${ + fromToken?.name + }` + ); + setNetworkFee( + `${Number(tx.result.commission_amounts[0][1]) / 10 ** 7} ${ + fromToken?.name + }` + ); - setTokenAmounts((prevAmounts) => { - const newToTokenAmount = Number(tx.result.ask_amount) / 10 ** 7; + // Only update if the amount has actually changed + const newToTokenAmount = Number(tx.result.ask_amount) / 10 ** 7; + setTokenAmounts((prevAmounts) => { + if (Math.abs(prevAmounts[1] - newToTokenAmount) > 0.000001) { return [prevAmounts[0], newToTokenAmount]; - }); - } - } catch (e) { - console.log(e); + } + return prevAmounts; + }); } + } catch (e) { + console.log(e); + } finally { setLoadingSimulate(false); } - }, [fromToken?.name, toToken, fromAmount, operations, tokenAmounts]); + }, [fromToken, toToken, operations, tokenAmounts[0], loadingSimulate]); /** * Handles user selecting a token from the asset selector. - * - * @param {Token} token - The token selected by the user. */ const handleTokenClick = useCallback( (token: Token): void => { if (isFrom) { - setTokens( - (tokens) => - [ - ...tokens.filter((el) => el.name !== token.name), - fromToken, - ].filter(Boolean) as Token[] - ); setFromToken(token); + // If the selected token was the toToken, swap them + if (toToken && token.name === toToken.name) { + setToToken(fromToken); + } + + // Update the tokens list, maintaining the current toToken + setTokens((prevTokens) => { + const filteredTokens = prevTokens.filter( + (t) => + t.name !== token.name && (!toToken || t.name !== toToken.name) + ); + // Add fromToken back to the list if it exists + return fromToken ? [...filteredTokens, fromToken] : filteredTokens; + }); } else { - setTokens( - (tokens) => - [...tokens.filter((el) => el.name !== token.name), toToken].filter( - Boolean - ) as Token[] - ); setToToken(token); + // If the selected token was the fromToken, swap them + if (fromToken && token.name === fromToken.name) { + setFromToken(toToken); + } + + // Update the tokens list, maintaining the current fromToken + setTokens((prevTokens) => { + const filteredTokens = prevTokens.filter( + (t) => + t.name !== token.name && (!fromToken || t.name !== fromToken.name) + ); + // Add toToken back to the list if it exists + return toToken ? [...filteredTokens, toToken] : filteredTokens; + }); } setAssetSelectorOpen(false); + // Reset simulation flags when token changes + operationsUpdateComplete.current = false; }, - [fromToken, isFrom, toToken] + [fromToken, toToken, isFrom] ); /** * Opens the asset selector. - * - * @param {boolean} isFromToken - Whether the asset selector is for the "from" token. */ const handleSelectorOpen = useCallback((isFromToken: boolean): void => { setAssetSelectorOpen(true); setIsFrom(isFromToken); }, []); - // Effect hook to fetch all tokens once the component mounts + /** + * Load pools data from the contract - minimizing API calls + */ + const loadPoolsData = useCallback(async () => { + if (hasPoolsLoaded.current) return allPools; + + try { + const factoryContract = new PhoenixFactoryContract.Client({ + contractId: constants.FACTORY_ADDRESS, + networkPassphrase: constants.NETWORK_PASSPHRASE, + rpcUrl: constants.RPC_URL, + }); + const { result } = await factoryContract.query_all_pools_details(); + + const allPairs = result.map((pool: any) => ({ + asset_a: pool.pool_response.asset_a.address, + asset_b: pool.pool_response.asset_b.address, + })); + + hasPoolsLoaded.current = true; + setAllPools(allPairs); + appStore.setLoading(false); + return allPairs; + } catch (error) { + console.error("Failed to load pools data:", error); + appStore.setLoading(false); + return []; + } + }, [appStore]); + + // Effect hook to fetch all tokens once the component mounts - with better controls useEffect(() => { - const getAllTokens = async (): Promise => { + if (!isClient || hasTokensLoaded.current) return; + + const getAllTokens = async () => { + // Avoid multiple loads + tokenLoadCount.current += 1; + if (tokenLoadCount.current > 1) return; + + if (appStore.allTokens.length > 0) { + // Avoid fetching if we already have tokens + setIsLoading(false); + setTokens(appStore.allTokens.slice(2)); + setFromToken(appStore.allTokens[0]); + setToToken(appStore.allTokens[1]); + hasTokensLoaded.current = true; + appStore.setLoading(false); + + // Still load pools in the background + setTimeout(() => { + loadPoolsData(); + }, 500); + + return; + } + setIsLoading(true); try { + // Load tokens first const allTokens = await appStore.getAllTokens(); - setTokens(allTokens.slice(2)); - setFromToken(allTokens[0]); - setToToken(allTokens[1]); - setIsLoading(false); - - // Get all pools - const factoryContract = new PhoenixFactoryContract.Client({ - contractId: constants.FACTORY_ADDRESS, - networkPassphrase: constants.NETWORK_PASSPHRASE, - rpcUrl: constants.RPC_URL, - }); - const { result } = await factoryContract.query_all_pools_details(); - const allPairs = result.map((pool: any) => ({ - asset_a: pool.pool_response.asset_a.address, - asset_b: pool.pool_response.asset_b.address, - })); - setAllPools(allPairs); + if (allTokens?.length > 1) { + setTokens(allTokens.slice(2)); + setFromToken(allTokens[0]); + setToToken(allTokens[1]); + hasTokensLoaded.current = true; + } } catch (e) { - console.error(e); + console.error("Failed to load tokens:", e); } finally { + setIsLoading(false); appStore.setLoading(false); + + // Load pools separately regardless of token loading success + setTimeout(() => { + loadPoolsData(); + }, 500); } }; + getAllTokens(); - }, []); + }, [appStore, loadPoolsData, isClient]); - // Effect hook to simulate swaps on token change - useEffect(() => { - if (fromToken && toToken && operations.length > 0) { - doSimulateSwap(); + // Update operations when tokens change with stricter control to prevent infinite loops + const updateSwapOperations = useCallback(() => { + // Only run this once per token pair change + if ( + !fromToken || + !toToken || + !appStore.allTokens.length || + !allPools.length || + fromToken.name === toToken.name + ) { + return; } - }, [fromToken, toToken, fromAmount, operations.length]); - // Effect hook to update operations when tokens change + // Calculate current state hash to compare + const currentPairKey = `${fromToken.name}-${toToken.name}`; + + // Skip if operations are already up to date for this token pair + if (prevOperations.current === currentPairKey) { + return; + } + + // Find token contract IDs + const fromTokenContractID = appStore.allTokens.find( + (token: Token) => token.name === fromToken.name + )?.contractId; + + const toTokenContractID = appStore.allTokens.find( + (token: Token) => token.name === toToken.name + )?.contractId; + + if (!fromTokenContractID || !toTokenContractID) return; + + // Create a new best path + const { operations: ops } = findBestPath( + toTokenContractID, + fromTokenContractID, + allPools + ); + + if (!ops?.length) return; + + const _operations = ops.reverse(); + const _swapRoute = _operations + .map( + (op) => + appStore.allTokens.find( + (token: any) => token.contractId === op.ask_asset + )?.name + ) + .filter(Boolean); + + // Update operations and swap route + setOperations(_operations); + setSwapRoute(`${fromToken.name} -> ${_swapRoute.join(" -> ")}`); + + // Mark that operations have been updated and save current pair + prevOperations.current = currentPairKey; + operationsUpdateComplete.current = true; + + // Check trustline if wallet is connected + if (storePersist.wallet.address && toTokenContractID) { + handleTrustLine(toTokenContractID); + } + }, [ + allPools, + fromToken, + toToken, + appStore.allTokens, + storePersist.wallet.address, + handleTrustLine, + ]); + + // Update operations when tokens or pools change - with safeguards useEffect(() => { - if (fromToken && toToken) { - const fromTokenContractID = appStore.allTokens.find( - (token: Token) => token.name === fromToken.name - )?.contractId; - const toTokenContractID = appStore.allTokens.find( - (token: Token) => token.name === toToken.name - )?.contractId; - - if (!fromTokenContractID || !toTokenContractID) return; - - const { operations: ops } = findBestPath( - toTokenContractID, - fromTokenContractID, - allPools - ); - const _operations = ops.reverse(); - const _swapRoute = _operations - .map( - (op) => - appStore.allTokens.find( - (token: any) => token.contractId === op.ask_asset - )?.name - ) - .filter(Boolean); - - setOperations((prevOps) => - prevOps !== _operations ? _operations : prevOps - ); - setSwapRoute(`${fromToken.name} -> ${_swapRoute.join(" -> ")}`); - if (storePersist.wallet.address) { - handleTrustLine(toTokenContractID); - } + if (!isClient || !fromToken || !toToken || !allPools.length) return; + + // Update operations only if something meaningful has changed + updateSwapOperations(); + }, [ + isClient, + fromToken?.name, // Only depend on the name, not the whole object + toToken?.name, // Only depend on the name, not the whole object + allPools.length, // Only depend on the length, not the whole array + updateSwapOperations, + ]); + + // Simulate swap with debounced amount changes and throttling + useEffect(() => { + if (!isClient) return; + + // Skip if no operations or tokens + if ( + !fromToken || + !toToken || + !operations.length || + !operationsUpdateComplete.current + ) { + return; } - }, [allPools, fromToken?.name, toToken?.name, storePersist.wallet.address]); - /** - * Handles adding a trustline for a token. - * - * @param {string} tokenAddress - The address of the token. - * @async - */ - const handleTrustLine = useCallback( - async (tokenAddress: string): Promise => { - const trust = await checkTrustline( - storePersist.wallet.address!, - tokenAddress - ); - setTrustlineButtonActive(!trust.exists); - setTrustlineTokenSymbol(trust.asset?.code || ""); - const tlAsset = await appStore.fetchTokenInfo( - "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA" - ); - setTrustlineAssetAmount( - Number(tlAsset?.balance) / 10 ** tlAsset?.decimals! - ); - setTrustlineTokenName(trust.asset?.contract || ""); - }, - [storePersist.wallet.address] - ); + // Skip if amount is zero + if (fromAmount <= 0) { + if (tokenAmounts[1] !== 0) { + setTokenAmounts([tokenAmounts[0], 0]); + } + return; + } - /** - * Adds a trustline for the specified token. - * - * @async - */ - const addTrustLine = useCallback(async (): Promise => { - try { - setTxBroadcasting(true); - await fetchAndIssueTrustline( - storePersist.wallet.address!, - trustlineTokenName - ); - setTrustlineButtonActive(false); - } catch (e) { - console.log(e); + // Use throttled simulation with a minimum interval between calls + if (simulationRequestRef.current) { + clearTimeout(simulationRequestRef.current); } - setTxBroadcasting(false); - }, [storePersist.wallet.address, trustlineTokenName]); + + simulationRequestRef.current = setTimeout(() => { + doSimulateSwap(); + simulationRequestRef.current = null; + }, 500); + + // Cleanup on unmount or when dependencies change + return () => { + if (simulationRequestRef.current) { + clearTimeout(simulationRequestRef.current); + simulationRequestRef.current = null; + } + }; + }, [fromAmount, fromToken?.name, toToken?.name, operations, isClient]); + + // Memoized swap container props to prevent unnecessary re-renders + const swapContainerProps = useMemo( + () => ({ + onOptionsClick: () => setOptionsOpen(true), + onSwapTokensClick: () => { + setTokenAmounts((prevAmounts) => [prevAmounts[1], prevAmounts[0]]); + setFromToken(toToken); + setToToken(fromToken); + // Reset flags when tokens are swapped + operationsUpdateComplete.current = false; + prevOperations.current = ""; + }, + fromTokenValue: tokenAmounts[0].toString()!, + toTokenValue: tokenAmounts[1].toString()!, + fromToken: fromToken!, + toToken: toToken!, + onTokenSelectorClick: handleSelectorOpen, + onSwapButtonClick: doSwap, + onInputChange: (isFrom: boolean, value: string) => { + const numValue = Number(value) || 0; + setTokenAmounts((prevAmounts) => { + if (isFrom) { + if (prevAmounts[0] === numValue) return prevAmounts; + return [numValue, prevAmounts[1]]; + } else { + if (prevAmounts[1] === numValue) return prevAmounts; + return [prevAmounts[0], numValue]; + } + }); + }, + exchangeRate, + networkFee, + route: swapRoute, + loadingSimulate, + estSellPrice: "TODO", + minSellPrice: "TODO", + slippageTolerance: `${maxSpread}%`, + swapButtonDisabled: tokenAmounts[0] <= 0 || !storePersist.wallet.address, + trustlineButtonActive, + trustlineAssetName: trustlineTokenSymbol, + trustlineButtonDisabled: trustlineAssetAmount < 0.5, + onTrustlineButtonClick: addTrustLine, + }), + [ + tokenAmounts, + fromToken, + toToken, + exchangeRate, + networkFee, + swapRoute, + loadingSimulate, + maxSpread, + trustlineButtonActive, + trustlineTokenSymbol, + trustlineAssetAmount, + storePersist.wallet.address, + doSwap, + handleSelectorOpen, + addTrustLine, + ] + ); return ( <> - {/* Hacky Title Injector - Waiting for Next Helmet for Next15 */} - + {/* Hacky Title Injector - Only on client side */} + {isClient && ( + + )} - {isLoading ? ( + {isLoading || !isClient ? ( ) : ( - + {!optionsOpen && !assetSelectorOpen && fromToken && toToken && ( - setOptionsOpen(true)} - onSwapTokensClick={() => { - setTokenAmounts((prevAmounts) => [ - prevAmounts[1], - prevAmounts[0], - ]); - setFromToken(toToken); - setToToken(fromToken); - }} - fromTokenValue={tokenAmounts[0].toString()} - toTokenValue={tokenAmounts[1].toString()} - fromToken={fromToken} - toToken={toToken} - onTokenSelectorClick={(isFromToken: boolean) => - handleSelectorOpen(isFromToken) - } - onSwapButtonClick={() => doSwap()} - onInputChange={(isFrom: boolean, value: string) => { - setTokenAmounts((prevAmounts) => - isFrom - ? [Number(value), 0] - : [prevAmounts[0], Number(value)] - ); - }} - exchangeRate={exchangeRate} - networkFee={networkFee} - route={swapRoute} - loadingSimulate={loadingSimulate} - estSellPrice={"TODO"} - minSellPrice={"TODO"} - slippageTolerance={`${maxSpread}%`} - swapButtonDisabled={ - tokenAmounts[0] <= 0 || - storePersist.wallet.address === undefined - } - trustlineButtonActive={trustlineButtonActive} - trustlineAssetName={trustlineTokenSymbol} - trustlineButtonDisabled={trustlineAssetAmount < 0.5} - onTrustlineButtonClick={() => addTrustLine()} - /> + )} {optionsOpen && ( @@ -416,12 +644,14 @@ export default function SwapPage(): JSX.Element { animate={{ opacity: 1 }} exit={{ opacity: 0 }} > - setOptionsOpen(false)} - onChange={(option: string) => setMaxSpread(Number(option))} - /> + + setOptionsOpen(false)} + onChange={(option: number) => setMaxSpread(option)} + /> + )} {assetSelectorOpen && ( @@ -431,12 +661,14 @@ export default function SwapPage(): JSX.Element { exit={{ opacity: 0 }} > {tokens.length > 0 ? ( - setAssetSelectorOpen(false)} - onTokenClick={handleTokenClick} - /> + + setAssetSelectorOpen(false)} + onTokenClick={handleTokenClick} + /> + ) : ( setAssetSelectorOpen(false)} diff --git a/packages/core/components/SideNav/SideNav.tsx b/packages/core/components/SideNav/SideNav.tsx index c269ccfb..b6574ba0 100644 --- a/packages/core/components/SideNav/SideNav.tsx +++ b/packages/core/components/SideNav/SideNav.tsx @@ -43,6 +43,19 @@ const SideNav = ({ active: pathname == "/swap", href: "/swap", }, + { + label: "Earn", + icon: ( + Earn Icon + ), + active: pathname == "/earn", + href: "/earn", + }, { label: "Pools", icon: ( @@ -57,10 +70,10 @@ const SideNav = ({ href: "/pools", }, { - label: "Trade History", + label: "Statistics", icon: ( History Icon void; +} + const contractClients = { pair: PhoenixPairContract.Client, multihop: PhoenixMultihopContract.Client, @@ -55,6 +60,7 @@ interface BaseExecuteContractTransactionParams { client: ContractClientType, restore?: boolean ) => Promise>; + options?: TransactionOptions; } interface ExecuteContractTransactionParams @@ -110,6 +116,7 @@ export const useContractTransaction = () => { contractType, contractAddress, transactionFunction, + options = {}, }: ExecuteContractTransactionParams) => { const signer = getSigner(storePersist, appStore); const networkPassphrase = constants.NETWORK_PASSPHRASE; @@ -137,16 +144,18 @@ export const useContractTransaction = () => { console.log("Attempting to sign and send transaction..."); - // Step 2: Handle signing and sending the transaction with async toast - const promise = new Promise<{ transactionId?: string }>( - async (resolve, reject) => { + // Step 2: Handle signing and sending with the new addAsyncToast + return addAsyncToast( + new Promise(async (resolve, reject) => { try { if (restore) { console.log("Restoring transaction state..."); await transaction.simulate({ restore: true }); + options.onSuccess?.(); // Call onSuccess callback after successful restore resolve({}); } else { const sentTransaction = await transaction.signAndSend(); + options.onSuccess?.(); // Call onSuccess callback after successful transaction resolve({ transactionId: sentTransaction.sendTransactionResponse?.hash, @@ -169,33 +178,29 @@ export const useContractTransaction = () => { "Error during restoring transaction:", restoreError ); - reject(restoreError); // Reject with the restoration error + reject(restoreError); } finally { closeRestoreModal(); } }); return; // Exit early since restore will handle the resolution } - - reject(error); // Reject with the error + reject(error); } - } + }), + loadingMessage, + { title: "Transaction" } ); - - // Add the promise to the toast for UI updates - addAsyncToast(promise, loadingMessage); - - // Delay at least 5 seconds before finishing the transaction process - await promise; - await new Promise((resolve) => setTimeout(resolve, 5000)); } catch (error) { console.log("Unexpected error executing contract transaction", error); - addAsyncToast(Promise.reject(error), loadingMessage); + return addAsyncToast(Promise.reject(error), loadingMessage, { + title: "Transaction", + }); } }; // Start transaction execution - await executeTransaction(); + return executeTransaction(); }, [addAsyncToast, storePersist, openRestoreModal, closeRestoreModal] ); diff --git a/packages/core/hooks/useToast.ts b/packages/core/hooks/useToast.ts index 6a37891f..9df3e4fb 100644 --- a/packages/core/hooks/useToast.ts +++ b/packages/core/hooks/useToast.ts @@ -1,11 +1,3 @@ // ToastHook.ts -import { useContext } from "react"; -import ToastContext from "@/providers/ToastProvider"; - -export const useToast = () => { - const context = useContext(ToastContext); - if (!context) { - throw new Error("useToast must be used within a ToastProvider"); - } - return context; -}; +// Re-export the UI library's useToast hook +export { useToast } from "@phoenix-protocol/ui/src/Common/Toast"; diff --git a/packages/core/next.config.js b/packages/core/next.config.js index c611954a..8c785949 100644 --- a/packages/core/next.config.js +++ b/packages/core/next.config.js @@ -2,6 +2,9 @@ const nextConfig = { experimental: { reactCompiler: true, + turbo: { + // Remove the problematic resolveAlias configuration + } }, webpack: (config) => { config.resolve.alias.canvas = false; diff --git a/packages/core/package.json b/packages/core/package.json index b2886bc8..fe855cd1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,9 +23,9 @@ "@vercel/analytics": "^1.1.3", "bufferutil": "^4.0.8", "eslint": "8.45.0", - "eslint-config-next": "15.1.3", + "eslint-config-next": "15.2.4", "moment-timezone": "^0.5.45", - "next": "15.1.3", + "next": "15.2.4", "pino-pretty": "^11.1.0", "react": "19.0.0", "react-dom": "19.0.0", @@ -38,15 +38,15 @@ }, "devDependencies": { "@types/node": "20.4.2", - "@types/react": "19.0.2", - "@types/react-dom": "19.0.2", + "@types/react": "19.0.12", + "@types/react-dom": "19.0.4", "@types/react-google-recaptcha": "^2.1.9", "babel-plugin-react-compiler": "^19.0.0-beta-0dec889-20241115", "css-loader": "^6.8.1", "style-loader": "^3.3.3" }, "resolutions": { - "@types/react": "19.0.2", - "@types/react-dom": "19.0.2" + "@types/react": "19.0.12", + "@types/react-dom": "19.0.4" } } diff --git a/packages/core/providers/ClientOnlyProvider.tsx b/packages/core/providers/ClientOnlyProvider.tsx new file mode 100644 index 00000000..ad569915 --- /dev/null +++ b/packages/core/providers/ClientOnlyProvider.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { ReactNode, useEffect, useState } from "react"; + +interface ClientOnlyProps { + children: ReactNode; + fallback?: ReactNode; +} + +/** + * ClientOnly component that renders its children only on the client-side + * to prevent hydration mismatch errors with client-specific code. + * + * @param {ClientOnlyProps} props - The component props + * @param {ReactNode} props.children - Content to render on client-side + * @param {ReactNode} props.fallback - Optional fallback content during SSR + */ +export default function ClientOnly({ + children, + fallback = null, +}: ClientOnlyProps) { + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + if (!isMounted) { + return <>{fallback}; + } + + return <>{children}; +} diff --git a/packages/core/providers/ToastProvider.tsx b/packages/core/providers/ToastProvider.tsx index 61907e24..9194676e 100644 --- a/packages/core/providers/ToastProvider.tsx +++ b/packages/core/providers/ToastProvider.tsx @@ -1,102 +1,18 @@ -// ToastProvider.tsx -import React, { createContext, useState, ReactNode, FC } from "react"; -import { Box } from "@mui/material"; -import { Toast } from "@/components/Toast"; - -// Define Toast type -interface ToastType { - id: number; - type: "success" | "error" | "loading"; - message: string; - transactionId?: string; -} - -// Define context value type -interface ToastContextType { - addToast: ( - message: string, - type: "success" | "error" | "loading", - transactionId?: string - ) => void; - addAsyncToast: ( - promise: Promise<{ transactionId?: string }>, - loadingMessage: string - ) => void; -} - -// Create Toast Context -const ToastContext = createContext(undefined); +import React, { ReactNode, FC } from "react"; +import { + ToastProvider as UIToastProvider, + ToastContainer, +} from "@phoenix-protocol/ui/src/Common/Toast"; export const ToastProvider: FC<{ children: ReactNode }> = ({ children }) => { - const [toasts, setToasts] = useState([]); - - const addToast = ( - message: string, - type: "success" | "error" | "loading", - transactionId?: string - ) => { - const id = Date.now(); - setToasts((prevToasts) => [ - ...prevToasts, - { id, type, message, transactionId }, - ]); - }; - - const removeToast = (id: number) => { - setToasts((prevToasts) => prevToasts.filter((toast) => toast.id !== id)); - }; - - // Function to add async toast - const addAsyncToast = async ( - promise: Promise<{ transactionId?: string }>, - loadingMessage: string - ) => { - const id = Date.now(); - // Show loading toast - addToast(loadingMessage, "loading"); - try { - const result = await promise; - // Update toast to success with transaction ID if present in the result - removeToast(id); - addToast( - "Transaction completed successfully!", - "success", - result.transactionId - ); - } catch (error: any) { - // Update toast to error - removeToast(id); - addToast( - error.message ? error.message : "Something went wrong.", - "error" - ); - } - }; - return ( - - {children} - - {toasts.map((toast) => ( - removeToast(toast.id)} - /> - ))} - - + + {children as any} + {/* ToastContainer will now automatically use toasts from context */} + + ); }; -export default ToastContext; +export { useToast } from "@phoenix-protocol/ui/src/Common/Toast"; +export default ToastProvider; diff --git a/packages/state/src/state/persist/createConnectWalletActions.ts b/packages/state/src/state/persist/createConnectWalletActions.ts index fa4b75a1..447987da 100644 --- a/packages/state/src/state/persist/createConnectWalletActions.ts +++ b/packages/state/src/state/persist/createConnectWalletActions.ts @@ -58,9 +58,9 @@ export const createConnectWalletActions = () => { network: "STANDALONE", networkPassphrase: "Public Global Stellar Network ; September 2015", networkUrl: - "https://mainnet.stellar.validationcloud.io/v1/YcyPYotN_b6-_656rpr0CabDwlGgkT42NCzPVIqcZh0", + "https://horizon-testnet.stellar.org", sorobanRpcUrl: - "https://bitter-alpha-layer.stellar-mainnet.quiknode.pro/54b50c548864e1470fd52dbd629b647d556b983e", + "https://horizon-testnet.stellar.org", }; // Throw an error if the network is not supported. diff --git a/packages/state/src/state/wallet/actions.ts b/packages/state/src/state/wallet/actions.ts index 3f3cceb3..546ab741 100644 --- a/packages/state/src/state/wallet/actions.ts +++ b/packages/state/src/state/wallet/actions.ts @@ -11,8 +11,9 @@ import { SorobanTokenContract, } from "@phoenix-protocol/contracts"; import { usePersistStore } from "../store"; -import { constants, fetchTokenPrices } from "@phoenix-protocol/utils"; +import { constants, fetchTokenPrices, Signer, TradeAPi } from "@phoenix-protocol/utils"; import { LiquidityPoolInfo } from "@phoenix-protocol/contracts/build/phoenix-pair"; +import { signTransaction } from "@stellar/freighter-api"; const getCategory = (name: string) => { switch (name.toLowerCase()) { @@ -53,32 +54,35 @@ export const createWalletActions = ( let parsedResults: LiquidityPoolInfo[]; try { const publicKey = address || constants.TESTING_SOURCE.accountId(); - + console.log(1) // Factory contract const factoryContract = new PhoenixFactoryContract.Client({ publicKey, contractId: constants.FACTORY_ADDRESS, networkPassphrase: constants.NETWORK_PASSPHRASE, rpcUrl: constants.RPC_URL, + }); // Fetch all available tokens from chain const allPoolsDetails = await factoryContract.query_all_pools_details(); - + console.log(2) // Parse results parsedResults = allPoolsDetails.result; } catch (e) { const publicKey = constants.TESTING_SOURCE.accountId(); - + console.log(3) // Factory contract const factoryContract = new PhoenixFactoryContract.Client({ publicKey, contractId: constants.FACTORY_ADDRESS, networkPassphrase: constants.NETWORK_PASSPHRASE, rpcUrl: constants.RPC_URL, + signTransaction: (xdr: string) => new Signer().sign(xdr) }); // Fetch all available tokens from chain - const allPoolsDetails = await factoryContract.query_all_pools_details(); - + const _allPoolsDetails = await factoryContract.query_all_pools_details({ simulate: false}); + const allPoolsDetails = await _allPoolsDetails.simulate({restore: true}); + console.log(4) // Parse results parsedResults = allPoolsDetails.result; } @@ -102,6 +106,8 @@ export const createWalletActions = ( await Promise.all(allAssets); + const tradeAPI = new TradeAPi.API(constants.TRADING_API_URL); + const _tokens = getState() .tokens.filter( (token: Token) => @@ -126,7 +132,7 @@ export const createWalletActions = ( usdValue: Number( token?.symbol === "PHO" ? await fetchPho() - : await fetchTokenPrices(token?.symbol) + : await tradeAPI.getPrice(token?.id) ).toFixed(2), contractId: token?.id, }; diff --git a/packages/state/src/state/wallet/freighter.ts b/packages/state/src/state/wallet/freighter.ts index 76f0edd1..b2c2321a 100644 --- a/packages/state/src/state/wallet/freighter.ts +++ b/packages/state/src/state/wallet/freighter.ts @@ -5,7 +5,7 @@ export function freighter(): Connector { return { id: "freighter", name: "Freighter", - iconUrl: "https://app.phoenix-hub.io/freighter.png", + iconUrl: "/freighter.svg", iconBackground: "#fff", installed: true, downloadUrls: { @@ -20,7 +20,7 @@ export function freighter(): Connector { return { ...(await freighterApi.getNetworkDetails()), networkUrl: - "https://mainnet.stellar.validationcloud.io/v1/YcyPYotN_b6-_656rpr0CabDwlGgkT42NCzPVIqcZh0", + "https://horizon-testnet.stellar.org", }; }, async getPublicKey(): Promise { diff --git a/packages/state/src/state/wallet/hana.ts b/packages/state/src/state/wallet/hana.ts index fc5cb5d2..fba1f5f6 100644 --- a/packages/state/src/state/wallet/hana.ts +++ b/packages/state/src/state/wallet/hana.ts @@ -64,7 +64,7 @@ export function hana(): Connector { return { ...(await freighterApi.getNetworkDetails()), networkUrl: - "https://mainnet.stellar.validationcloud.io/v1/YcyPYotN_b6-_656rpr0CabDwlGgkT42NCzPVIqcZh0", + "https://horizon-testnet.stellar.org", }; }, async isAvailable(): Promise { diff --git a/packages/state/src/state/wallet/xbull.ts b/packages/state/src/state/wallet/xbull.ts index 00a0f39e..a274260d 100644 --- a/packages/state/src/state/wallet/xbull.ts +++ b/packages/state/src/state/wallet/xbull.ts @@ -22,7 +22,7 @@ export function xbull(): Connector { return { ...(await freighterApi.getNetworkDetails()), networkUrl: - "https://mainnet.stellar.validationcloud.io/v1/YcyPYotN_b6-_656rpr0CabDwlGgkT42NCzPVIqcZh0", + "https://horizon-testnet.stellar.org", }; }, async isAvailable(): Promise { diff --git a/packages/strategies/.gitignore b/packages/strategies/.gitignore new file mode 100644 index 00000000..f4ba78f1 --- /dev/null +++ b/packages/strategies/.gitignore @@ -0,0 +1,7 @@ +/node_modules +package-lock.json +/build +/.rollup.cache +/module +/main +/types \ No newline at end of file diff --git a/packages/strategies/package.json b/packages/strategies/package.json new file mode 100644 index 00000000..ed997e54 --- /dev/null +++ b/packages/strategies/package.json @@ -0,0 +1,17 @@ +{ + "name": "@phoenix-protocol/strategies", + "version": "0.1.0", + "description": "Strategy provider registry for Phoenix DeFi", + "main": "build/index.js", + "types": "build/index.d.ts", + "scripts": { + "build": "tsc", + "dev": "tsc --watch" + }, + "dependencies": { + "@phoenix-protocol/types": "*" + }, + "devDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/packages/strategies/src/debug.ts b/packages/strategies/src/debug.ts new file mode 100644 index 00000000..9ec3651d --- /dev/null +++ b/packages/strategies/src/debug.ts @@ -0,0 +1,31 @@ +import { StrategyRegistry } from "./registry"; +import { PhoenixBoostProvider } from "./phoenix/provider"; + +async function debugRegistry() { + console.log("=== STRATEGY REGISTRY DEBUG ==="); + + // Initialize provider + const provider = new PhoenixBoostProvider(); + console.log("Provider created:", provider.id); + + // Register provider + StrategyRegistry.registerProvider(provider); + console.log("Provider registered"); + + // List providers + const providers = StrategyRegistry.getProviders(); + console.log( + "Providers:", + providers.map((p) => p.id) + ); + + // Get strategies + const strategies = await StrategyRegistry.getAllStrategies(); + console.log("Total strategies:", strategies.length); + strategies.forEach(async (strategy, index) => { + const meta = await strategy.getMetadata(); + console.log(`Strategy ${index + 1}:`, meta.id); + }); +} + +export { debugRegistry }; diff --git a/packages/strategies/src/index.ts b/packages/strategies/src/index.ts new file mode 100644 index 00000000..396ed238 --- /dev/null +++ b/packages/strategies/src/index.ts @@ -0,0 +1,18 @@ +// Core exports +export * from "./types"; +export { StrategyRegistry } from "./registry"; +export * from "./phoenix/provider"; +export { default as PhoenixBoostStrategy } from "./phoenix/strategies/pho-usdc.liquidity"; + +// Initialize registry with providers +import { StrategyRegistry } from "./registry"; +import { PhoenixBoostProvider } from "./phoenix/provider"; + +// Register all strategy providers +const setupRegistry = () => { + // Register the Phoenix provider + StrategyRegistry.registerProvider(new PhoenixBoostProvider()); +}; + +// Initialize the registry +setupRegistry(); diff --git a/packages/strategies/src/phoenix/provider.ts b/packages/strategies/src/phoenix/provider.ts new file mode 100644 index 00000000..06897a0f --- /dev/null +++ b/packages/strategies/src/phoenix/provider.ts @@ -0,0 +1,48 @@ +import { Strategy, StrategyProvider } from "../types"; +import PhoenixPhoUsdcStrategy from "./strategies/pho-usdc.liquidity"; +import PhoenixXlmPhoStrategy from "./strategies/xlm-pho.liquidity"; +import PhoenixXlmUsdcStrategy from "./strategies/xlm-usdc.liquidity"; + +export class PhoenixBoostProvider implements StrategyProvider { + id = "phoenix-boost"; + name = "Phoenix Boost"; + domain = "phoenix-hub.io"; + description = "Official staking strategies from Phoenix Protocol"; + icon = "/cryptoIcons/pho.svg"; + + private strategies: Strategy[] = []; + + constructor() { + // Initialize with the PHO-USDC strategy + this.strategies.push( + new PhoenixPhoUsdcStrategy(), + new PhoenixXlmPhoStrategy(), + new PhoenixXlmUsdcStrategy() + ); + console.log( + "Phoenix provider initialized with strategies:", + this.strategies.length + ); + } + + async getTVL(): Promise { + try { + const strategyTVLs = await Promise.all( + this.strategies.map(async (strategy) => { + const metadata = await strategy.getMetadata(); + return metadata.tvl; + }) + ); + + return strategyTVLs.reduce((total, current) => total + current, 0); + } catch (error) { + console.error("Error getting TVL for Phoenix provider:", error); + return 0; + } + } + + async getStrategies(): Promise { + console.log("Returning Phoenix strategies:", this.strategies.length); + return this.strategies; + } +} diff --git a/packages/strategies/src/phoenix/strategies/pho-usdc.liquidity.ts b/packages/strategies/src/phoenix/strategies/pho-usdc.liquidity.ts new file mode 100644 index 00000000..67d5a0b8 --- /dev/null +++ b/packages/strategies/src/phoenix/strategies/pho-usdc.liquidity.ts @@ -0,0 +1,423 @@ +import { Strategy, StrategyMetadata } from "../../types"; +import { useAppStore, usePersistStore } from "@phoenix-protocol/state"; +import { + API, + constants, + fetchTokenPrices, + Signer, +} from "@phoenix-protocol/utils"; +import { + PhoenixPairContract, + PhoenixStakeContract, + fetchPho, // Ensure AssembledTransaction is available +} from "@phoenix-protocol/contracts"; + +import { AssembledTransaction } from "@stellar/stellar-sdk/lib/contract"; + +// Needed constants and types +const userWalletAddress = usePersistStore.getState().wallet; +const contractAddress = + "CD5XNKK3B6BEF2N7ULNHHGAMOKZ7P6456BFNIHRF4WNTEDKBRWAE7IAA"; +const contractType = "pair"; + +class PhoenixPhoUsdcStrategy implements Strategy { + private metadata: StrategyMetadata = { + id: "phoenix-provide-liquidity-pho-usdc", + providerId: "phoenix-pho-usdc", + name: "Provide Liquidity to PHO-USDC", + description: "Provide liquidity to the PHO-USDC pair and earn PHO rewards", + assets: [], + tvl: 0, + apr: 0, + rewardToken: { + name: "PHO", + icon: "/cryptoIcons/pho.svg", + amount: 0, + category: "phoenix", + usdValue: 0, + }, + unbondTime: 0, + category: "liquidity", + available: true, + contractAddress, + contractType, + }; + + private initialized: boolean = false; + private pairContract: PhoenixPairContract.Client | null = null; + private stakeContract: PhoenixStakeContract.Client | null = null; + private stakeContractAddress: string = ""; + private tokenA: any = null; + private tokenB: any = null; + private lpToken: any = null; + private userStake: number = 0; + private userRewards: any[] = []; + private lpTokenPrice: number = 0; + + constructor() { + // Initialize immediately + this.initialize(); + } + + // Async initialization method + private async initialize(): Promise { + try { + // Initialize contract clients + this.pairContract = new PhoenixPairContract.Client({ + contractId: contractAddress, + networkPassphrase: constants.NETWORK_PASSPHRASE, + rpcUrl: constants.RPC_URL, + }); + + // Fetch real-time data for the strategy + await this.fetchPoolDetails(); + + // Mark as initialized + this.initialized = true; + console.log("PhoenixBoostStrategy initialized successfully"); + } catch (error) { + console.error("Failed to initialize PhoenixBoostStrategy:", error); + } + } + + private async fetchPoolDetails(): Promise { + try { + const store = useAppStore.getState(); + const storePersist = usePersistStore.getState(); + + // Fetch pool config and info from chain + const [pairConfig, pairInfo] = await Promise.all([ + this.pairContract?.query_config(), + this.pairContract?.query_pool_info(), + ]); + + if (pairConfig?.result && pairInfo?.result) { + // Set stake contract address and instantiate client + this.stakeContractAddress = pairConfig.result.stake_contract.toString(); + this.stakeContract = new PhoenixStakeContract.Client({ + contractId: this.stakeContractAddress, + networkPassphrase: constants.NETWORK_PASSPHRASE, + rpcUrl: constants.RPC_URL, + signTransaction: (tx: string) => new Signer().sign(tx), + publicKey: storePersist.wallet.address, + }); + + // Fetch token info + const [_tokenA, _tokenB, _lpToken] = await Promise.all([ + store.fetchTokenInfo(pairConfig.result.token_a), + store.fetchTokenInfo(pairConfig.result.token_b), + store.fetchTokenInfo(pairConfig.result.share_token, true), + ]); + + this.tokenA = _tokenA; + this.tokenB = _tokenB; + this.lpToken = _lpToken; + + // Fetch token prices and calculate TVL + const [priceA, priceB] = await Promise.all([ + API.getPrice(_tokenA?.symbol || ""), + API.getPrice(_tokenB?.symbol || ""), + ]); + + // Calculate pool TVL + const tvl = + (priceA * Number(pairInfo.result.asset_a.amount)) / + 10 ** Number(_tokenA?.decimals) + + (priceB * Number(pairInfo.result.asset_b.amount)) / + 10 ** Number(_tokenB?.decimals); + + this.metadata.tvl = tvl; + + // Calculate APR based on incentives + const stakingInfoA = await this.stakeContract.query_total_staked({ + simulate: false, + }); + const stakingInfo = await stakingInfoA.simulate({ restore: true }); + const totalStaked = Number(stakingInfo?.result); + + const ratioStaked = + totalStaked / Number(pairInfo.result.asset_lp_share.amount); + const valueStaked = tvl * ratioStaked; + + // Apply the same pool incentives logic + const poolIncentives = [ + { + // XLM / USDC + address: "CBHCRSVX3ZZ7EGTSYMKPEFGZNWRVCSESQR3UABET4MIW52N4EVU6BIZX", + amount: 12500, + }, + // XLM/PHO + { + address: "CBCZGGNOEUZG4CAAE7TGTQQHETZMKUT4OIPFHHPKEUX46U4KXBBZ3GLH", + amount: 25000, + }, + { + // PHO/USDC + address: "CD5XNKK3B6BEF2N7ULNHHGAMOKZ7P6456BFNIHRF4WNTEDKBRWAE7IAA", + amount: 18750, + }, + ]; + + const poolIncentive = poolIncentives.find( + (incentive) => incentive.address === contractAddress + )!; + + const phoprice = await fetchPho(); + const apr = + ((poolIncentive?.amount * phoprice) / valueStaked) * 100 * 6; + + this.metadata.apr = apr / 100; + + // Update reward token USD value + this.metadata.rewardToken.usdValue = phoprice; + + // Calculate LP token price for value conversion + this.lpTokenPrice = valueStaked / (totalStaked / 10 ** 7); + + // Set assets in metadata + this.metadata.assets = [ + { + name: _tokenA?.symbol!, + icon: `/cryptoIcons/${_tokenA?.symbol.toLowerCase()}.svg`, + usdValue: priceA, + amount: + Number(pairInfo.result.asset_a.amount) / + 10 ** Number(_tokenA?.decimals), + category: "phoenix", + }, + { + name: _tokenB?.symbol!, + icon: `/cryptoIcons/${_tokenB?.symbol.toLowerCase()}.svg`, + usdValue: priceB, + amount: + Number(pairInfo.result.asset_b.amount) / + 10 ** Number(_tokenB?.decimals), + category: "phoenix", + }, + ]; + + // If wallet connected, fetch user stake and rewards + if (storePersist.wallet.address) { + await this.fetchUserPosition(storePersist.wallet.address); + } + } + } catch (error) { + console.error("Error fetching pool details:", error); + } + } + + private async fetchUserPosition(walletAddress: string): Promise { + try { + if (!this.stakeContract) return; + + // Get user stakes + const stakesQuery = await this.stakeContract.query_staked( + { address: walletAddress }, + { simulate: false } + ); + + const stakes = await stakesQuery.simulate({ restore: true }); + + // Calculate total staked amount + if (stakes?.result && stakes.result.stakes.length > 0) { + const _stakes = stakes.result.stakes.reduce( + (total: number, stake: any) => + total + Number(stake.stake) / 10 ** (this.lpToken?.decimals || 7), + 0 + ); + this.userStake = this.lpTokenPrice * _stakes; + } else { + this.userStake = 0; + } + + // Fetch user rewards + const rewards = await this.stakeContract.query_withdrawable_rewards({ + user: walletAddress, + }); + + if (rewards?.result?.rewards) { + const store = useAppStore.getState(); + const rewardPromises = rewards.result.rewards.map( + async (reward: any) => { + const token = await store.fetchTokenInfo(reward.reward_address); + return { + name: token?.symbol.toUpperCase(), + icon: `/cryptoIcons/${token?.symbol.toLowerCase()}.svg`, + usdValue: await API.getPrice(token?.symbol || ""), + amount: + Number(reward.reward_amount.toString()) / + 10 ** token?.decimals!, + category: "phoenix", + }; + } + ); + this.userRewards = await Promise.all(rewardPromises); + } + } catch (error) { + console.error("Error fetching user position:", error); + } + } + + async getMetadata(): Promise { + // If not initialized yet, wait for initialization + if (!this.initialized) { + await this.waitForInitialization(); + } + return this.metadata; + } + + private async waitForInitialization(timeout = 5000): Promise { + const start = Date.now(); + while (!this.initialized && Date.now() - start < timeout) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + if (!this.initialized) { + console.warn( + "Strategy initialization timed out, returning potentially incomplete metadata" + ); + } + } + + async getUserStake(walletAddress: string): Promise { + if (walletAddress !== usePersistStore.getState().wallet.address) { + // If asking for a different wallet, fetch specific data + await this.fetchUserPosition(walletAddress); + } + return this.userStake; + } + + async hasUserJoined(walletAddress: string): Promise { + return (await this.getUserStake(walletAddress)) > 0; + } + + async getUserRewards(walletAddress: string): Promise { + if (walletAddress !== usePersistStore.getState().wallet.address) { + // If asking for a different wallet, fetch specific data + await this.fetchUserPosition(walletAddress); + } + + // Sum up all rewards + return this.userRewards.reduce( + (total: number, reward: any) => total + reward.amount, + 0 + ); + } + + // Update the bond method to handle multiple tokens for liquidity provision + async bond( + walletAddress: string, + amountA: number, + amountB?: number + ): Promise> { + if (amountB === undefined) { + throw new Error( + "Amount B is required for providing liquidity to this pair." + ); + } + // For liquidity pairs, we expect both amounts + return this.provideLiquidity(walletAddress, amountA, amountB); + } + + async unbond( + walletAddress: string, + amount: number + ): Promise> { + if (!this.stakeContract) { + throw new Error("Stake contract not initialized"); + } + if (amount <= 0) { + throw new Error("Unbond amount must be positive"); + } + + // Get user's stakes + const stakesQuery = await this.stakeContract.query_staked( + { address: walletAddress }, + { simulate: false } + ); + + const stakes = await stakesQuery.simulate({ restore: true }); + + if (!stakes?.result || stakes.result.stakes.length === 0) { + throw new Error("No stakes found for this user to unbond"); + } + + // Get the stake to unbond (using the first one as an example) + const stake = stakes.result.stakes[0]; + + const assembledTx = await this.stakeContract.unbond( + { + sender: walletAddress, + stake_amount: BigInt( + (amount * 10 ** (this.lpToken?.decimals || 7)).toFixed(0) + ), + stake_timestamp: BigInt(stake.stake_timestamp), + }, + { simulate: true } + ); + return assembledTx; + } + + async claim(walletAddress: string): Promise> { + if (!this.stakeContract) { + throw new Error("Stake contract not initialized"); + } + const assembledTx = await this.stakeContract.withdraw_rewards( + { sender: walletAddress }, + { simulate: true } + ); + return assembledTx; + } + + // Helper method to provide liquidity directly through this strategy + async provideLiquidity( + walletAddress: string, + tokenAAmount: number, + tokenBAmount: number + ): Promise> { + if (!this.pairContract) { + throw new Error("Pair contract not initialized"); + } + const assembledTx = await this.pairContract.provide_liquidity( + { + sender: walletAddress, + desired_a: BigInt( + (tokenAAmount * 10 ** (this.tokenA?.decimals || 7)).toFixed(0) + ), + desired_b: BigInt( + (tokenBAmount * 10 ** (this.tokenB?.decimals || 7)).toFixed(0) + ), + min_a: undefined, + min_b: undefined, + custom_slippage_bps: undefined, + deadline: undefined, + }, + { simulate: true } + ); + return assembledTx; + } + + // Helper method to remove liquidity + async removeLiquidity( + walletAddress: string, + lpAmount: number + ): Promise> { + if (!this.pairContract) { + throw new Error("Pair contract not initialized"); + } + const assembledTx = await this.pairContract.withdraw_liquidity( + { + sender: walletAddress, + share_amount: BigInt( + (lpAmount * 10 ** (this.lpToken?.decimals || 7)).toFixed(0) + ), + min_a: BigInt(1), + min_b: BigInt(1), + deadline: undefined, + }, + { simulate: true } + ); + return assembledTx; + } +} + +export default PhoenixPhoUsdcStrategy; diff --git a/packages/strategies/src/phoenix/strategies/xlm-pho.liquidity.ts b/packages/strategies/src/phoenix/strategies/xlm-pho.liquidity.ts new file mode 100644 index 00000000..ccd7f249 --- /dev/null +++ b/packages/strategies/src/phoenix/strategies/xlm-pho.liquidity.ts @@ -0,0 +1,430 @@ +import { Strategy, StrategyMetadata } from "../../types"; +import { useAppStore, usePersistStore } from "@phoenix-protocol/state"; +import { + API, + constants, + fetchTokenPrices, + Signer, +} from "@phoenix-protocol/utils"; +import { + PhoenixPairContract, + PhoenixStakeContract, + fetchPho, +} from "@phoenix-protocol/contracts"; +import { AssembledTransaction } from "@stellar/stellar-sdk/lib/contract"; + +// Needed constants and types +const contractAddress = + "CBCZGGNOEUZG4CAAE7TGTQQHETZMKUT4OIPFHHPKEUX46U4KXBBZ3GLH"; +const contractType = "pair"; + +class PhoenixXlmPhoStrategy implements Strategy { + private metadata: StrategyMetadata = { + id: "phoenix-provide-liquidity-xlm-pho", + providerId: "phoenix-xlm-pho", + name: "Provide Liquidity to XLM-PHO", + description: "Provide liquidity to the XLM-PHO pair and earn PHO rewards", + assets: [], + tvl: 0, + apr: 0, + rewardToken: { + name: "PHO", + icon: "/cryptoIcons/pho.svg", + amount: 0, + category: "phoenix", + usdValue: 0, + }, + unbondTime: 0, + category: "liquidity", + available: true, + contractAddress, + contractType, + }; + + private initialized: boolean = false; + private pairContract: PhoenixPairContract.Client | null = null; + private stakeContract: PhoenixStakeContract.Client | null = null; + private stakeContractAddress: string = ""; + private tokenA: any = null; + private tokenB: any = null; + private lpToken: any = null; + private userStake: number = 0; + private userRewards: any[] = []; + private lpTokenPrice: number = 0; + + constructor() { + // Initialize immediately + this.initialize(); + } + + // Async initialization method + private async initialize(): Promise { + try { + // Initialize contract clients + this.pairContract = new PhoenixPairContract.Client({ + contractId: contractAddress, + networkPassphrase: constants.NETWORK_PASSPHRASE, + rpcUrl: constants.RPC_URL, + }); + + // Fetch real-time data for the strategy + await this.fetchPoolDetails(); + + // Mark as initialized + this.initialized = true; + console.log("PhoenixBoostStrategy initialized successfully"); + } catch (error) { + console.error("Failed to initialize PhoenixBoostStrategy:", error); + } + } + + private async fetchPoolDetails(): Promise { + try { + const store = useAppStore.getState(); + const storePersist = usePersistStore.getState(); + + // Fetch pool config and info from chain + const [pairConfig, pairInfo] = await Promise.all([ + this.pairContract?.query_config(), + this.pairContract?.query_pool_info(), + ]); + + if (pairConfig?.result && pairInfo?.result) { + // Set stake contract address and instantiate client + this.stakeContractAddress = pairConfig.result.stake_contract.toString(); + this.stakeContract = new PhoenixStakeContract.Client({ + contractId: this.stakeContractAddress, + networkPassphrase: constants.NETWORK_PASSPHRASE, + rpcUrl: constants.RPC_URL, + signTransaction: (tx: string) => new Signer().sign(tx), + publicKey: storePersist.wallet.address, + }); + + // Fetch token info + const [_tokenA, _tokenB, _lpToken] = await Promise.all([ + store.fetchTokenInfo(pairConfig.result.token_a), + store.fetchTokenInfo(pairConfig.result.token_b), + store.fetchTokenInfo(pairConfig.result.share_token, true), + ]); + + this.tokenA = _tokenA; + this.tokenB = _tokenB; + this.lpToken = _lpToken; + + // Fetch token prices and calculate TVL + const [priceA, priceB] = await Promise.all([ + API.getPrice(_tokenA?.symbol || ""), + API.getPrice(_tokenB?.symbol || ""), + ]); + + // Calculate pool TVL + const tvl = + (priceA * Number(pairInfo.result.asset_a.amount)) / + 10 ** Number(_tokenA?.decimals) + + (priceB * Number(pairInfo.result.asset_b.amount)) / + 10 ** Number(_tokenB?.decimals); + + this.metadata.tvl = tvl; + + // Calculate APR based on incentives + const stakingInfoA = await this.stakeContract.query_total_staked({ + simulate: false, + }); + const stakingInfo = await stakingInfoA.simulate({ restore: true }); + const totalStaked = Number(stakingInfo?.result); + + const ratioStaked = + totalStaked / Number(pairInfo.result.asset_lp_share.amount); + const valueStaked = tvl * ratioStaked; + + // Apply the same pool incentives logic + const poolIncentives = [ + { + // XLM / USDC + address: "CBHCRSVX3ZZ7EGTSYMKPEFGZNWRVCSESQR3UABET4MIW52N4EVU6BIZX", + amount: 12500, + }, + // XLM/PHO + { + address: "CBCZGGNOEUZG4CAAE7TGTQQHETZMKUT4OIPFHHPKEUX46U4KXBBZ3GLH", + amount: 25000, + }, + { + // PHO/USDC + address: "CD5XNKK3B6BEF2N7ULNHHGAMOKZ7P6456BFNIHRF4WNTEDKBRWAE7IAA", + amount: 18750, + }, + ]; + + const poolIncentive = poolIncentives.find( + (incentive) => incentive.address === contractAddress + )!; + + const phoprice = await fetchPho(); + const apr = + ((poolIncentive?.amount * phoprice) / valueStaked) * 100 * 6; + + this.metadata.apr = apr / 100; + + // Update reward token USD value + this.metadata.rewardToken.usdValue = phoprice; + + // Calculate LP token price for value conversion + this.lpTokenPrice = valueStaked / (totalStaked / 10 ** 7); + + // Set assets in metadata + this.metadata.assets = [ + { + name: _tokenA?.symbol!, + icon: `/cryptoIcons/${_tokenA?.symbol.toLowerCase()}.svg`, + usdValue: priceA, + amount: + Number(pairInfo.result.asset_a.amount) / + 10 ** Number(_tokenA?.decimals), + category: "phoenix", + }, + { + name: _tokenB?.symbol!, + icon: `/cryptoIcons/${_tokenB?.symbol.toLowerCase()}.svg`, + usdValue: priceB, + amount: + Number(pairInfo.result.asset_b.amount) / + 10 ** Number(_tokenB?.decimals), + category: "phoenix", + }, + ]; + + // If wallet connected, fetch user stake and rewards + if (storePersist.wallet.address) { + await this.fetchUserPosition(storePersist.wallet.address); + } + } + } catch (error) { + console.error("Error fetching pool details:", error); + } + } + + private async fetchUserPosition(walletAddress: string): Promise { + try { + if (!this.stakeContract) return; + + // Get user stakes + const stakesQuery = await this.stakeContract.query_staked( + { address: walletAddress }, + { simulate: false } + ); + + const stakes = await stakesQuery.simulate({ restore: true }); + + // Calculate total staked amount + if (stakes?.result && stakes.result.stakes.length > 0) { + const _stakes = stakes.result.stakes.reduce( + (total: number, stake: any) => + total + Number(stake.stake) / 10 ** (this.lpToken?.decimals || 7), + 0 + ); + this.userStake = this.lpTokenPrice * _stakes; + } else { + this.userStake = 0; + } + + // Fetch user rewards + const rewards = await this.stakeContract.query_withdrawable_rewards({ + user: walletAddress, + }); + + if (rewards?.result?.rewards) { + const store = useAppStore.getState(); + const rewardPromises = rewards.result.rewards.map( + async (reward: any) => { + const token = await store.fetchTokenInfo(reward.reward_address); + return { + name: token?.symbol.toUpperCase(), + icon: `/cryptoIcons/${token?.symbol.toLowerCase()}.svg`, + usdValue: await API.getPrice(token?.symbol || ""), + amount: + Number(reward.reward_amount.toString()) / + 10 ** token?.decimals!, + category: "phoenix", + }; + } + ); + + this.userRewards = await Promise.all(rewardPromises); + } + } catch (error) { + console.error("Error fetching user position:", error); + } + } + + async getMetadata(): Promise { + // If not initialized yet, wait for initialization + if (!this.initialized) { + await this.waitForInitialization(); + } + return this.metadata; + } + + private async waitForInitialization(timeout = 5000): Promise { + const start = Date.now(); + while (!this.initialized && Date.now() - start < timeout) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + if (!this.initialized) { + console.warn( + "Strategy initialization timed out, returning potentially incomplete metadata" + ); + } + } + + async getUserStake(walletAddress: string): Promise { + if (walletAddress !== usePersistStore.getState().wallet.address) { + // If asking for a different wallet, fetch specific data + await this.fetchUserPosition(walletAddress); + } + return this.userStake; + } + + async hasUserJoined(walletAddress: string): Promise { + return (await this.getUserStake(walletAddress)) > 0; + } + + async getUserRewards(walletAddress: string): Promise { + if (walletAddress !== usePersistStore.getState().wallet.address) { + // If asking for a different wallet, fetch specific data + await this.fetchUserPosition(walletAddress); + } + + // Sum up all rewards + return this.userRewards.reduce( + (total: number, reward: any) => total + reward.amount, + 0 + ); + } + + // Update the bond method to handle multiple tokens for liquidity provision + async bond( + walletAddress: string, + amountA: number, + amountB?: number + ): Promise> { + if (amountB === undefined) { + throw new Error( + "Amount B is required for providing liquidity to this pair." + ); + } + // For liquidity pairs, we expect both amounts + return this.provideLiquidity(walletAddress, amountA, amountB); + } + + async unbond( + walletAddress: string, + amount: number + ): Promise> { + if (!this.stakeContract) { + throw new Error("Stake contract not initialized"); + } + if (amount <= 0) { + throw new Error("Unbond amount must be positive"); + } + + // Get user's stakes + const stakesQuery = await this.stakeContract.query_staked( + { address: walletAddress }, + { simulate: false } + ); + + const stakes = await stakesQuery.simulate({ restore: true }); + + if (!stakes?.result || stakes.result.stakes.length === 0) { + throw new Error("No stakes found for this user to unbond"); + } + + // Get the stake to unbond (using the first one as an example) + // In a real implementation, you might want to select a specific stake + const stake = stakes.result.stakes[0]; + + const assembledTx = await this.stakeContract.unbond( + { + sender: walletAddress, + stake_amount: BigInt( + (amount * 10 ** (this.lpToken?.decimals || 7)).toFixed(0) + ), + stake_timestamp: BigInt(stake.stake_timestamp), + }, + { simulate: true } + ); + + return assembledTx; + } + + async claim(walletAddress: string): Promise> { + if (!this.stakeContract) { + throw new Error("Stake contract not initialized"); + } + + const assembledTx = await this.stakeContract.withdraw_rewards( + { sender: walletAddress }, + { simulate: true } + ); + + return assembledTx; + } + + // Helper method to provide liquidity directly through this strategy + async provideLiquidity( + walletAddress: string, + tokenAAmount: number, + tokenBAmount: number + ): Promise> { + if (!this.pairContract) { + throw new Error("Pair contract not initialized"); + } + + const assembledTx = await this.pairContract.provide_liquidity( + { + sender: walletAddress, + desired_a: BigInt( + (tokenAAmount * 10 ** (this.tokenA?.decimals || 7)).toFixed(0) + ), + desired_b: BigInt( + (tokenBAmount * 10 ** (this.tokenB?.decimals || 7)).toFixed(0) + ), + min_a: undefined, + min_b: undefined, + custom_slippage_bps: undefined, + deadline: undefined, + }, + { simulate: true } + ); + + return assembledTx; + } + + // Helper method to remove liquidity + async removeLiquidity( + walletAddress: string, + lpAmount: number + ): Promise> { + if (!this.pairContract) { + throw new Error("Pair contract not initialized"); + } + + const assembledTx = await this.pairContract.withdraw_liquidity( + { + sender: walletAddress, + share_amount: BigInt( + (lpAmount * 10 ** (this.lpToken?.decimals || 7)).toFixed(0) + ), + min_a: BigInt(1), // Consider making these configurable or based on slippage + min_b: BigInt(1), // Consider making these configurable or based on slippage + deadline: undefined, + }, + { simulate: true } + ); + + return assembledTx; + } +} + +export default PhoenixXlmPhoStrategy; diff --git a/packages/strategies/src/phoenix/strategies/xlm-usdc.liquidity.ts b/packages/strategies/src/phoenix/strategies/xlm-usdc.liquidity.ts new file mode 100644 index 00000000..06c2d218 --- /dev/null +++ b/packages/strategies/src/phoenix/strategies/xlm-usdc.liquidity.ts @@ -0,0 +1,461 @@ +import { Strategy, StrategyMetadata } from "../../types"; +import { useAppStore, usePersistStore } from "@phoenix-protocol/state"; +import { + API, + constants, + fetchTokenPrices, + Signer, +} from "@phoenix-protocol/utils"; +import { + PhoenixPairContract, + PhoenixStakeContract, + fetchPho, +} from "@phoenix-protocol/contracts"; +import { AssembledTransaction } from "@stellar/stellar-sdk/lib/contract"; + +// Needed constants and types +const contractAddress = + "CBHCRSVX3ZZ7EGTSYMKPEFGZNWRVCSESQR3UABET4MIW52N4EVU6BIZX"; +const contractType = "pair"; + +class PhoenixXlmUsdcStrategy implements Strategy { + private metadata: StrategyMetadata = { + id: "phoenix-provide-liquidity-xlm-usdc", + providerId: "phoenix-xlm-usdc", + name: "Provide Liquidity to XLM-USDC", + description: "Provide liquidity to the XLM-USDC pair and earn PHO rewards", + assets: [], + tvl: 0, + apr: 0, + rewardToken: { + name: "PHO", + icon: "/cryptoIcons/pho.svg", + amount: 0, + category: "phoenix", + usdValue: 0, + }, + unbondTime: 0, + category: "liquidity", + available: true, + contractAddress, + contractType, + }; + + private initialized: boolean = false; + private pairContract: PhoenixPairContract.Client | null = null; + private stakeContract: PhoenixStakeContract.Client | null = null; + private stakeContractAddress: string = ""; + private tokenA: any = null; + private tokenB: any = null; + private lpToken: any = null; + private userStake: number = 0; + private userRewards: any[] = []; + private userIndividualStakesDetailed: any[] = []; // Store raw stakes for unbonding + private lpTokenPrice: number = 0; + + constructor() { + // Initialize immediately + this.initialize(); + } + + // Async initialization method + private async initialize(): Promise { + try { + // Initialize contract clients + this.pairContract = new PhoenixPairContract.Client({ + contractId: contractAddress, + networkPassphrase: constants.NETWORK_PASSPHRASE, + rpcUrl: constants.RPC_URL, + }); + + // Fetch real-time data for the strategy + await this.fetchPoolDetails(); + + // Mark as initialized + this.initialized = true; + console.log("PhoenixBoostStrategy initialized successfully"); + } catch (error) { + console.error("Failed to initialize PhoenixBoostStrategy:", error); + } + } + + private async fetchPoolDetails(): Promise { + try { + const store = useAppStore.getState(); + const storePersist = usePersistStore.getState(); + + // Fetch pool config and info from chain + const [pairConfig, pairInfo] = await Promise.all([ + this.pairContract?.query_config(), + this.pairContract?.query_pool_info(), + ]); + + if (pairConfig?.result && pairInfo?.result) { + // Set stake contract address and instantiate client + this.stakeContractAddress = pairConfig.result.stake_contract.toString(); + this.stakeContract = new PhoenixStakeContract.Client({ + contractId: this.stakeContractAddress, + networkPassphrase: constants.NETWORK_PASSPHRASE, + rpcUrl: constants.RPC_URL, + signTransaction: (tx: string) => new Signer().sign(tx), + publicKey: storePersist.wallet.address, + }); + + // Fetch token info + const [_tokenA, _tokenB, _lpToken] = await Promise.all([ + store.fetchTokenInfo(pairConfig.result.token_a), + store.fetchTokenInfo(pairConfig.result.token_b), + store.fetchTokenInfo(pairConfig.result.share_token, true), + ]); + + this.tokenA = _tokenA; + this.tokenB = _tokenB; + this.lpToken = _lpToken; + + // Fetch token prices and calculate TVL + const [priceA, priceB] = await Promise.all([ + API.getPrice(_tokenA?.symbol || ""), + API.getPrice(_tokenB?.symbol || ""), + ]); + + // Calculate pool TVL + const tvl = + (priceA * Number(pairInfo.result.asset_a.amount)) / + 10 ** Number(_tokenA?.decimals) + + (priceB * Number(pairInfo.result.asset_b.amount)) / + 10 ** Number(_tokenB?.decimals); + + this.metadata.tvl = tvl; + + // Calculate APR based on incentives + const stakingInfoA = await this.stakeContract.query_total_staked({ + simulate: false, + }); + const stakingInfo = await stakingInfoA.simulate({ restore: true }); + const totalStaked = Number(stakingInfo?.result); + + const ratioStaked = + totalStaked / Number(pairInfo.result.asset_lp_share.amount); + const valueStaked = tvl * ratioStaked; + + // Apply the same pool incentives logic + const poolIncentives = [ + { + // XLM / USDC + address: "CBHCRSVX3ZZ7EGTSYMKPEFGZNWRVCSESQR3UABET4MIW52N4EVU6BIZX", + amount: 12500, + }, + // XLM/PHO + { + address: "CBCZGGNOEUZG4CAAE7TGTQQHETZMKUT4OIPFHHPKEUX46U4KXBBZ3GLH", + amount: 25000, + }, + { + // PHO/USDC + address: "CD5XNKK3B6BEF2N7ULNHHGAMOKZ7P6456BFNIHRF4WNTEDKBRWAE7IAA", + amount: 18750, + }, + ]; + + const poolIncentive = poolIncentives.find( + (incentive) => incentive.address === contractAddress + )!; + + const phoprice = await fetchPho(); + const apr = + ((poolIncentive?.amount * phoprice) / valueStaked) * 100 * 6; + + this.metadata.apr = apr / 100; + + // Update reward token USD value + this.metadata.rewardToken.usdValue = phoprice; + + // Calculate LP token price for value conversion + this.lpTokenPrice = valueStaked / (totalStaked / 10 ** 7); + + // Set assets in metadata + this.metadata.assets = [ + { + name: _tokenA?.symbol!, + icon: `/cryptoIcons/${_tokenA?.symbol.toLowerCase()}.svg`, + usdValue: priceA, + amount: + Number(pairInfo.result.asset_a.amount) / + 10 ** Number(_tokenA?.decimals), + category: "phoenix", + }, + { + name: _tokenB?.symbol!, + icon: `/cryptoIcons/${_tokenB?.symbol.toLowerCase()}.svg`, + usdValue: priceB, + amount: + Number(pairInfo.result.asset_b.amount) / + 10 ** Number(_tokenB?.decimals), + category: "phoenix", + }, + ]; + + // If wallet connected, fetch user stake and rewards + if (storePersist.wallet.address) { + await this.fetchUserPosition(storePersist.wallet.address); + } + } + } catch (error) { + console.error("Error fetching pool details:", error); + } + } + + private async fetchUserPosition(walletAddress: string): Promise { + try { + if (!this.stakeContract) return; + + // Get user stakes + const stakesQuery = await this.stakeContract.query_staked( + { address: walletAddress }, + { simulate: false } + ); + + const stakesResult = await stakesQuery.simulate({ restore: true }); + this.userIndividualStakesDetailed = stakesResult?.result?.stakes || []; // Store raw stakes + + // Calculate total staked amount and populate userIndividualStakes for metadata + if (this.userIndividualStakesDetailed.length > 0) { + let totalLpStaked = BigInt(0); + this.metadata.userIndividualStakes = + this.userIndividualStakesDetailed.map((stake: any) => { + const lpAmountBigInt = BigInt(stake.stake); + totalLpStaked += lpAmountBigInt; + const lpAmountNumber = + Number(lpAmountBigInt) / 10 ** (this.lpToken?.decimals || 7); + return { + lpAmount: lpAmountBigInt, + timestamp: BigInt(stake.stake_timestamp), + displayAmount: `${lpAmountNumber.toFixed( + this.lpToken?.decimals || 7 + )} LP`, + displayDate: new Date( + Number(stake.stake_timestamp) * 1000 + ).toLocaleDateString(), + }; + }); + const totalLpStakedNumber = + Number(totalLpStaked) / 10 ** (this.lpToken?.decimals || 7); + this.userStake = this.lpTokenPrice * totalLpStakedNumber; + } else { + this.userStake = 0; + this.metadata.userIndividualStakes = []; + } + + // Fetch user rewards + const rewards = await this.stakeContract.query_withdrawable_rewards({ + user: walletAddress, + }); + + if (rewards?.result?.rewards) { + const store = useAppStore.getState(); + const rewardPromises = rewards.result.rewards.map( + async (reward: any) => { + const token = await store.fetchTokenInfo(reward.reward_address); + return { + name: token?.symbol.toUpperCase(), + icon: `/cryptoIcons/${token?.symbol.toLowerCase()}.svg`, + usdValue: await API.getPrice(token?.symbol || ""), + amount: + Number(reward.reward_amount.toString()) / + 10 ** token?.decimals!, + category: "phoenix", + }; + } + ); + + this.userRewards = await Promise.all(rewardPromises); + } + } catch (error) { + console.error("Error fetching user position:", error); + } + } + + async getMetadata(): Promise { + // If not initialized yet, wait for initialization + if (!this.initialized) { + await this.waitForInitialization(); + } + return this.metadata; + } + + private async waitForInitialization(timeout = 5000): Promise { + const start = Date.now(); + while (!this.initialized && Date.now() - start < timeout) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + if (!this.initialized) { + console.warn( + "Strategy initialization timed out, returning potentially incomplete metadata" + ); + } + } + + async getUserStake(walletAddress: string): Promise { + if (walletAddress !== usePersistStore.getState().wallet.address) { + // If asking for a different wallet, fetch specific data + await this.fetchUserPosition(walletAddress); + } + return this.userStake; + } + + async hasUserJoined(walletAddress: string): Promise { + return (await this.getUserStake(walletAddress)) > 0; + } + + async getUserRewards(walletAddress: string): Promise { + if (walletAddress !== usePersistStore.getState().wallet.address) { + // If asking for a different wallet, fetch specific data + await this.fetchUserPosition(walletAddress); + } + + // Sum up all rewards + return this.userRewards.reduce( + (total: number, reward: any) => total + reward.amount, + 0 + ); + } + + // Update the bond method to handle multiple tokens for liquidity provision + async bond( + walletAddress: string, + amountA: number, + amountB?: number + ): Promise> { + if (amountB === undefined) { + throw new Error( + "Amount B is required for providing liquidity to this pair." + ); + } + // For liquidity pairs, we expect both amounts + return this.provideLiquidity(walletAddress, amountA, amountB); + } + + async unbond( + walletAddress: string, + params: number | { lpAmount: bigint; timestamp: bigint } + ): Promise> { + if (!this.stakeContract) { + throw new Error("Stake contract not initialized"); + } + + let stakeAmountToUnbond: bigint; + let stakeTimestampToUnbond: bigint; + + if (typeof params === "number") { + if (this.userIndividualStakesDetailed.length === 0) { + throw new Error("No stakes available to unbond based on USD amount."); + } + if (this.lpTokenPrice <= 0) { + throw new Error( + "LP token price is not available to calculate unbond amount from USD." + ); + } + const lpAmountToUnbondNumber = params / this.lpTokenPrice; + stakeAmountToUnbond = BigInt( + (lpAmountToUnbondNumber * 10 ** (this.lpToken?.decimals || 7)).toFixed( + 0 + ) + ); + // This logic for 'number' param might need to be more robust, + // e.g. selecting which stake to unbond from or ensuring sufficient balance. + // For now, using the first stake's timestamp if a general amount is given. + // The primary UI flow should use the object { lpAmount, timestamp }. + const firstStake = this.userIndividualStakesDetailed[0]; + if (!firstStake || BigInt(firstStake.stake) < stakeAmountToUnbond) { + throw new Error( + "Not enough in the first stake or no stake available for general amount unbonding." + ); + } + stakeTimestampToUnbond = BigInt(firstStake.stake_timestamp); + // Fallback: if a number is passed, unbond the whole first stake. This is a simplification. + // stakeAmountToUnbond = BigInt(this.userIndividualStakesDetailed[0].stake); + // stakeTimestampToUnbond = BigInt(this.userIndividualStakesDetailed[0].stake_timestamp); + } else { + stakeAmountToUnbond = params.lpAmount; + stakeTimestampToUnbond = params.timestamp; + } + + if (stakeAmountToUnbond <= BigInt(0)) { + throw new Error("Unbond amount must be positive."); + } + + const assembledTx = await this.stakeContract.unbond( + { + sender: walletAddress, + stake_amount: stakeAmountToUnbond, + stake_timestamp: stakeTimestampToUnbond, + }, + { simulate: true } + ); + return assembledTx; + } + + async claim(walletAddress: string): Promise> { + if (!this.stakeContract) { + throw new Error("Stake contract not initialized"); + } + const assembledTx = await this.stakeContract.withdraw_rewards( + { sender: walletAddress }, + { simulate: true } + ); + return assembledTx; + } + + // Helper method to provide liquidity directly through this strategy + async provideLiquidity( + walletAddress: string, + tokenAAmount: number, + tokenBAmount: number + ): Promise> { + if (!this.pairContract) { + throw new Error("Pair contract not initialized"); + } + const assembledTx = await this.pairContract.provide_liquidity( + { + sender: walletAddress, + desired_a: BigInt( + (tokenAAmount * 10 ** (this.tokenA?.decimals || 7)).toFixed(0) + ), + desired_b: BigInt( + (tokenBAmount * 10 ** (this.tokenB?.decimals || 7)).toFixed(0) + ), + min_a: undefined, + min_b: undefined, + custom_slippage_bps: undefined, + deadline: undefined, + }, + { simulate: true } + ); + return assembledTx; + } + + // Helper method to remove liquidity + async removeLiquidity( + walletAddress: string, + lpAmount: number + ): Promise> { + if (!this.pairContract) { + throw new Error("Pair contract not initialized"); + } + const assembledTx = await this.pairContract.withdraw_liquidity( + { + sender: walletAddress, + share_amount: BigInt( + (lpAmount * 10 ** (this.lpToken?.decimals || 7)).toFixed(0) + ), + min_a: BigInt(1), + min_b: BigInt(1), + deadline: undefined, + }, + { simulate: true } + ); + return assembledTx; + } +} + +export default PhoenixXlmUsdcStrategy; diff --git a/packages/strategies/src/registry.ts b/packages/strategies/src/registry.ts new file mode 100644 index 00000000..a3146305 --- /dev/null +++ b/packages/strategies/src/registry.ts @@ -0,0 +1,88 @@ +import { StrategyProvider, Strategy, StrategyMetadata } from "./types"; + +export class StrategyRegistry { + private static providers: Map = new Map(); + private static strategiesCache: Map = new Map(); + private static metadataCache: Map = new Map(); + + static registerProvider(provider: StrategyProvider): void { + if (this.providers.has(provider.id)) { + console.warn( + `Provider with ID ${provider.id} is already registered. Skipping.` + ); + return; + } + this.providers.set(provider.id, provider); + } + + static getProvider(id: string): StrategyProvider | undefined { + return this.providers.get(id); + } + + static getProviders(): StrategyProvider[] { + return Array.from(this.providers.values()); + } + + static async getProviderTVL(providerId: string): Promise { + const provider = this.providers.get(providerId); + if (!provider) return 0; + return provider.getTVL(); + } + + static async getTotalTVL(): Promise { + const allProviders = this.getProviders(); + const tvls = await Promise.all( + allProviders.map((provider) => provider.getTVL()) + ); + return tvls.reduce((total, current) => total + current, 0); + } + + static async getStrategiesByProvider( + providerId: string + ): Promise { + const provider = this.providers.get(providerId); + if (!provider) return []; + + if (!this.strategiesCache.has(providerId)) { + const strategies = await provider.getStrategies(); + this.strategiesCache.set(providerId, strategies); + } + + return this.strategiesCache.get(providerId) || []; + } + + static async getAllStrategies(): Promise { + const allProviders = this.getProviders(); + const strategiesArrays = await Promise.all( + allProviders.map((provider) => this.getStrategiesByProvider(provider.id)) + ); + return strategiesArrays.flat(); + } + + static async getUserStrategies(walletAddress: string): Promise { + if (!walletAddress) return []; + + const allStrategies = await this.getAllStrategies(); + const userStrategies = await Promise.all( + allStrategies.map(async (strategy) => { + const hasJoined = await strategy.hasUserJoined(walletAddress); + return hasJoined ? strategy : null; + }) + ); + + return userStrategies.filter(Boolean) as Strategy[]; + } + + static async getStrategyMetadata( + strategy: Strategy + ): Promise { + const metadata = await strategy.getMetadata(); + this.metadataCache.set(metadata.id, metadata); + return metadata; + } + + static clearCache(): void { + this.strategiesCache.clear(); + this.metadataCache.clear(); + } +} diff --git a/packages/strategies/src/types.ts b/packages/strategies/src/types.ts new file mode 100644 index 00000000..964a2bf4 --- /dev/null +++ b/packages/strategies/src/types.ts @@ -0,0 +1,82 @@ +import { Token as BaseToken } from "@phoenix-protocol/types"; +import { AssembledTransaction } from "@stellar/stellar-sdk/lib/contract"; + +// Extend the Token interface to include address +export interface Token extends BaseToken { + address?: string; // Add address field that's used in stories +} + +// Define Contract Types - Mirroring useContractTransaction +export type ContractType = + | "pair" + | "multihop" + | "stake" + | "factory" + | "vesting" + | "token"; + +export interface StrategyProvider { + id: string; + name: string; + domain: string; + description?: string; + icon?: string; + getTVL(): Promise; + getStrategies(): Promise; +} + +// Define the structure for an individual stake/bond +export interface IndividualStake { + lpAmount: bigint; // Raw LP token amount for contract interaction + timestamp: bigint; // Raw timestamp for contract interaction + displayAmount: string; // User-friendly display of the amount (e.g., "123.45 LP") + displayDate: string; // User-friendly display of the stake date + // Potentially other identifiers or display info if needed +} + +export interface StrategyMetadata { + id: string; + providerId: string; + name: string; + description: string; + assets: Token[]; + tvl: number; + apr: number; + rewardToken: Token; + unbondTime: number; // seconds + link?: string; + category: string; + icon?: string; + available: boolean; + userStake?: number; // Amount the user has staked + userRewards?: number; // Rewards available for claiming + hasJoined?: boolean; // Whether the user has joined this strategy + // Add contract details for transactions + contractAddress: string; + contractType: ContractType; + userIndividualStakes?: IndividualStake[]; // For strategies with multiple, distinct stakes + // UI state properties + isMobile?: boolean; // For UI rendering + userAssetMatch?: boolean; // Used in StrategiesTable filtering +} + +export interface Strategy { + getMetadata(): Promise; + getUserStake(walletAddress: string): Promise; + hasUserJoined(walletAddress: string): Promise; + getUserRewards(walletAddress: string): Promise; + + // Update bond signature to support multiple token amounts + bond( + walletAddress: string, + amountA: number, + amountB?: number + ): Promise>; + + // Updated unbond signature to handle specific stakes or general amounts + unbond( + walletAddress: string, + params: number | { lpAmount: bigint; timestamp: bigint } + ): Promise>; + claim(walletAddress: string): Promise>; +} diff --git a/packages/strategies/tsconfig.json b/packages/strategies/tsconfig.json new file mode 100644 index 00000000..77a07384 --- /dev/null +++ b/packages/strategies/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "build", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/types/src/ui/AssetInfoModal.ts b/packages/types/src/ui/AssetInfoModal.ts index 05a4fc66..cdd2f69c 100644 --- a/packages/types/src/ui/AssetInfoModal.ts +++ b/packages/types/src/ui/AssetInfoModal.ts @@ -1,7 +1,38 @@ +import { Pool } from "./Pools"; + export interface AssetInfoModalProps { open: boolean; onClose: () => void; asset: AssetInfo; + userBalance: number; + loading: boolean; + tradingVolume7d: { + date?: { + day?: number; + month?: number; + year: number; + }; + time?: { + hour: number; + date?: { + day: number; + month: number; + year: number; + }; + }; + week?: { + week: number; + year: number; + }; + month?: { + month: number; + year: number; + }; + tokenAVolume: string; + tokenBVolume: string; + usdVolume: number; + }[]; + pools: Pool[]; } export interface AssetInfo { diff --git a/packages/types/src/ui/SlippageSettings.ts b/packages/types/src/ui/SlippageSettings.ts index 4f30f19e..2df47d66 100644 --- a/packages/types/src/ui/SlippageSettings.ts +++ b/packages/types/src/ui/SlippageSettings.ts @@ -1,6 +1,6 @@ export interface SlippageOptionsProps { - options: string[]; + options: number[]; selectedOption: number; onClose: () => void; - onChange: (option: string) => void; + onChange: (option: number) => void; } diff --git a/packages/ui/.storybook/babel.config.js b/packages/ui/.storybook/babel.config.js new file mode 100644 index 00000000..0862c0a5 --- /dev/null +++ b/packages/ui/.storybook/babel.config.js @@ -0,0 +1,12 @@ +module.exports = { + presets: [ + '@babel/preset-env', + ['@babel/preset-react', { runtime: 'automatic' }], + '@babel/preset-typescript', + ], + plugins: [ + '@babel/plugin-transform-runtime', + ['@babel/plugin-proposal-class-properties', { loose: true }], + '@babel/plugin-proposal-object-rest-spread' + ], +}; diff --git a/packages/ui/.storybook/main.ts b/packages/ui/.storybook/main.ts index 90052ff7..78f16d7f 100644 --- a/packages/ui/.storybook/main.ts +++ b/packages/ui/.storybook/main.ts @@ -1,38 +1,46 @@ import type { StorybookConfig } from "@storybook/react-webpack5"; +import { join, dirname } from "path"; +import * as path from 'path'; const config: StorybookConfig = { - stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], + stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], addons: [ "@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions", - "@storybook/addon-styling", + "@storybook/addon-viewport", ], + // Simplify TypeScript options + typescript: { + check: false, + reactDocgen: false, // Disable docgen to avoid processing issues + }, framework: { name: "@storybook/react-webpack5", - options: {}, + options: { + builder: { + useSWC: false, + } + }, }, docs: { autodocs: "tag", }, - staticDirs: ["../public"], // Static asset folder configuration + staticDirs: ["../public"], + // Use external webpack config webpackFinal: async (config) => { - // Ensure TypeScript loader is properly configured - config.module?.rules?.push({ - test: /\.tsx?$/, - use: [ - { - loader: "ts-loader", - options: { - transpileOnly: true, - }, - }, - ], - exclude: /node_modules/, - }); - - return config; + const customWebpackConfig = require('./webpack.config'); + return customWebpackConfig({ config }); + }, + core: { + disableTelemetry: true, + // Ensure csf-plugin doesn't interfere with processing + disableWhatsNewNotifications: true, }, }; export default config; + +function getAbsolutePath(value: string): any { + return dirname(require.resolve(join(value, "package.json"))); +} diff --git a/packages/ui/.storybook/preview.ts b/packages/ui/.storybook/preview.ts index 66c96b70..5fe49d9e 100644 --- a/packages/ui/.storybook/preview.ts +++ b/packages/ui/.storybook/preview.ts @@ -1,9 +1,9 @@ import type { Preview } from "@storybook/react"; -import { withThemeFromJSXProvider } from "@storybook/addon-styling"; -import { CssBaseline } from "@mui/material"; -import { ThemeProvider } from "../src"; +// Import theme directly without MUI components to avoid emotion issues at this level import theme from "../src/Theme"; import { INITIAL_VIEWPORTS } from "@storybook/addon-viewport"; +import React from "react"; +import { withMuiTheme } from "./withMuiTheme.decorator"; const customViewports = { iPhone12: { @@ -72,18 +72,13 @@ const preview: Preview = { date: /Date$/, }, }, + backgrounds: { + default: "dark", + }, }, + tags: ["autodocs"], + // Just use the decorator we created separately + decorators: [withMuiTheme], }; -export const decorators = [ - withThemeFromJSXProvider({ - themes: { - dark: theme, - }, - defaultTheme: "dark", - Provider: ThemeProvider, - GlobalStyles: CssBaseline, - }), -]; - export default preview; diff --git a/packages/ui/.storybook/preview.tsx b/packages/ui/.storybook/preview.tsx new file mode 100644 index 00000000..18a7e94e --- /dev/null +++ b/packages/ui/.storybook/preview.tsx @@ -0,0 +1,80 @@ +import type { Preview } from "@storybook/react"; +import { INITIAL_VIEWPORTS } from "@storybook/addon-viewport"; +import { withMuiTheme } from "./withMuiTheme.decorator"; +import React from "react"; + +const customViewports = { + iPhone12: { + name: "iPhone 12", + styles: { + width: "390px", + height: "844px", + }, + }, + iPhone12Pro: { + name: "iPhone 12 Pro", + styles: { + width: "390px", + height: "844px", + }, + }, + iPhone12ProMax: { + name: "iPhone 12 Pro Max", + styles: { + width: "428px", + height: "926px", + }, + }, + iPhone12Mini: { + name: "iPhone 12 Mini", + styles: { + width: "360px", + height: "780px", + }, + }, + kindleFireHD: { + name: "Kindle Fire HD", + styles: { + width: "533px", + height: "801px", + }, + }, + desktopFullHd: { + name: "Desktop Full HD", + styles: { + width: "1920px", + height: "1080px", + }, + }, + desktop4k: { + name: "Desktop 4K", + styles: { + width: "3840px", + height: "2160px", + }, + }, +}; + +const preview: Preview = { + parameters: { + viewport: { + viewports: { + ...INITIAL_VIEWPORTS, + ...customViewports, + }, + }, + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + backgrounds: { + default: "dark", + }, + }, + decorators: [withMuiTheme], +}; + +export default preview; diff --git a/packages/ui/.storybook/setupFile.js b/packages/ui/.storybook/setupFile.js new file mode 100644 index 00000000..a3a6f99b --- /dev/null +++ b/packages/ui/.storybook/setupFile.js @@ -0,0 +1,15 @@ +// This file sets up global mocks or imports for use in Storybook + +// Safely provide emotion modules if we're in a browser environment +if (typeof window !== 'undefined') { + try { + window.emotion = { + react: require('@emotion/react'), + styled: require('@emotion/styled') + }; + } catch (error) { + console.warn('Could not load emotion packages:', error); + } +} + +// You can add other global setup code here if needed diff --git a/packages/ui/.storybook/tsconfig.json b/packages/ui/.storybook/tsconfig.json new file mode 100644 index 00000000..7300377a --- /dev/null +++ b/packages/ui/.storybook/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "strict": false, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true + }, + "include": [ + "../src/**/*", + "./**/*" + ], + "exclude": [ + "../node_modules" + ] +} diff --git a/packages/ui/.storybook/webpack.config.js b/packages/ui/.storybook/webpack.config.js new file mode 100644 index 00000000..045bbd72 --- /dev/null +++ b/packages/ui/.storybook/webpack.config.js @@ -0,0 +1,58 @@ +const path = require('path'); +const fs = require('fs'); + +module.exports = ({ config }) => { + // Find the absolute path to the workspace root + const workspaceRoot = path.resolve(__dirname, '../../../'); + + // Add packages to resolve.modules + config.resolve.modules = [ + ...(config.resolve.modules || []), + path.resolve(__dirname, '../node_modules'), + path.resolve(workspaceRoot, 'node_modules'), + ]; + + // Set alias for emotion packages + config.resolve.alias = { + ...config.resolve.alias, + '@emotion/react': path.resolve(workspaceRoot, 'node_modules/@emotion/react'), + '@emotion/styled': path.resolve(workspaceRoot, 'node_modules/@emotion/styled'), + }; + + // Make sure TypeScript extensions are handled + config.resolve.extensions = [ + ...(config.resolve.extensions || []), + '.ts', '.tsx', '.js', '.jsx', '.mjs' + ]; + + // Clear out any conflicting rules for .ts/.tsx files + config.module.rules = config.module.rules.filter(rule => { + if (!rule.test) return true; + const test = rule.test.toString(); + return !(test.includes('ts') || test.includes('tsx')); + }); + + // Add unified rules for all JavaScript and TypeScript files + config.module.rules.push({ + test: /\.(jsx?|tsx?)$/, + exclude: /node_modules/, + use: [ + { + loader: require.resolve('babel-loader'), + options: { + presets: [ + require.resolve('@babel/preset-env'), + require.resolve('@babel/preset-react'), + require.resolve('@babel/preset-typescript') + ], + plugins: [ + require.resolve('@babel/plugin-transform-runtime'), + [require.resolve('@babel/plugin-proposal-class-properties'), { loose: true }] + ] + } + } + ] + }); + + return config; +}; diff --git a/packages/ui/.storybook/withMuiTheme.decorator.tsx b/packages/ui/.storybook/withMuiTheme.decorator.tsx new file mode 100644 index 00000000..6a84d559 --- /dev/null +++ b/packages/ui/.storybook/withMuiTheme.decorator.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { CssBaseline, ThemeProvider } from "@mui/material"; +import theme from "../src/Theme"; + +export const withMuiTheme = (StoryFn: React.FC) => { + return ( + + + + + ); +}; diff --git a/packages/ui/doctor-storybook.log b/packages/ui/doctor-storybook.log new file mode 100644 index 00000000..cc0d2526 --- /dev/null +++ b/packages/ui/doctor-storybook.log @@ -0,0 +1,21 @@ +🩺 The doctor is checking the health of your Storybook.. +╭ Incompatible packages found ────────────────────────────────────────╮ +│ │ +│ The following packages are incompatible with Storybook 8.6.9 as │ +│ they depend on different major versions of Storybook packages: │ +│ - @storybook/addon-styling@1.3.7 │ +│ │ +│ │ +│ Please consider updating your packages or contacting the │ +│ maintainers for compatibility details. │ +│ For more on Storybook 8 compatibility, see the linked GitHub │ +│ issue: │ +│ https://github.com/storybookjs/storybook/issues/26031 │ +│ │ +╰─────────────────────────────────────────────────────────────────────╯ + +You can always recheck the health of your project by running: +npx storybook doctor + +Full logs are available in /Users/milan/workspace/phoenix/frontend/packages/ui/doctor-storybook.log + diff --git a/packages/ui/package.json b/packages/ui/package.json index d1515ab3..0e6732c0 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -37,21 +37,23 @@ "@babel/preset-env": "7.19.1", "@babel/preset-react": "7.18.6", "@babel/preset-typescript": "7.16.7", + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", "@phoenix-protocol/state": "workspace:^", "@phoenix-protocol/types": "workspace:^", "@rollup/plugin-commonjs": "^25.0.0", "@rollup/plugin-node-resolve": "^15.0.2", "@rollup/plugin-typescript": "^11.1.1", - "@storybook/addon-actions": "v8.5.0-alpha.9", - "@storybook/addon-essentials": "v8.5.0-alpha.9", - "@storybook/addon-interactions": "v8.5.0-alpha.9", - "@storybook/addon-links": "v8.5.0-alpha.9", - "@storybook/addon-styling": "^1.3.0", - "@storybook/blocks": "v8.5.0-alpha.9", - "@storybook/preset-react-webpack": "8.5.0-alpha.9", - "@storybook/react": "v8.5.0-alpha.9", - "@storybook/react-webpack5": "v8.5.0-alpha.9", - "@storybook/testing-library": "^0.0.14-next.2", + "@storybook/addon-actions": "^8.6.9", + "@storybook/addon-essentials": "^8.6.9", + "@storybook/addon-interactions": "^8.6.9", + "@storybook/addon-links": "^8.6.9", + "@storybook/addon-viewport": "^8.6.9", + "@storybook/blocks": "^8.6.9", + "@storybook/preset-react-webpack": "^8.6.9", + "@storybook/react": "^8.6.9", + "@storybook/react-webpack5": "^8.6.9", + "@storybook/testing-library": "^0.2.2", "@types/react": "^18.0.27", "babel-loader": "^8.2.2", "postcss": "^8.4.21", @@ -62,7 +64,7 @@ "rollup-plugin-dts": "^5.3.0", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", - "storybook": "v8.5.0-alpha.9", + "storybook": "^8.6.9", "ts-loader": "^9.5.1", "typescript": "^5.0.4" }, @@ -78,8 +80,6 @@ "@mui/utils": "6.1.7" }, "dependencies": { - "@emotion/react": "^11.11.1", - "@emotion/styled": "^11.11.0", "@fontsource/inter": "^5.0.3", "@fontsource/poppins": "^5.0.3", "@fontsource/roboto": "^5.0.3", diff --git a/packages/ui/public/3d.png b/packages/ui/public/3d.png new file mode 100644 index 00000000..cfe3fcb1 Binary files /dev/null and b/packages/ui/public/3d.png differ diff --git a/packages/ui/public/earnIcon.svg b/packages/ui/public/earnIcon.svg new file mode 100644 index 00000000..fed1fa0b --- /dev/null +++ b/packages/ui/public/earnIcon.svg @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/ui/public/earnIconActive.svg b/packages/ui/public/earnIconActive.svg new file mode 100644 index 00000000..f74f5483 --- /dev/null +++ b/packages/ui/public/earnIconActive.svg @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/ui/public/freighter.svg b/packages/ui/public/freighter.svg new file mode 100644 index 00000000..3899d4cc --- /dev/null +++ b/packages/ui/public/freighter.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/packages/ui/public/plants.png b/packages/ui/public/plants.png new file mode 100644 index 00000000..e8d752dd Binary files /dev/null and b/packages/ui/public/plants.png differ diff --git a/packages/ui/public/saving.png b/packages/ui/public/saving.png new file mode 100644 index 00000000..f723902f Binary files /dev/null and b/packages/ui/public/saving.png differ diff --git a/packages/ui/public/wallet-1.png b/packages/ui/public/wallet-1.png new file mode 100644 index 00000000..967549bb Binary files /dev/null and b/packages/ui/public/wallet-1.png differ diff --git a/packages/ui/public/wallet-2.png b/packages/ui/public/wallet-2.png new file mode 100644 index 00000000..0000a192 Binary files /dev/null and b/packages/ui/public/wallet-2.png differ diff --git a/packages/ui/public/wallet-3.png b/packages/ui/public/wallet-3.png new file mode 100644 index 00000000..3b341208 Binary files /dev/null and b/packages/ui/public/wallet-3.png differ diff --git a/packages/ui/src/AnchorServices/AnchorServices.stories.tsx b/packages/ui/src/AnchorServices/AnchorServices.stories.tsx index b20e3c88..ab1da82f 100644 --- a/packages/ui/src/AnchorServices/AnchorServices.stories.tsx +++ b/packages/ui/src/AnchorServices/AnchorServices.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { AnchorServices } from "./AnchorServices"; import { Anchor } from "@phoenix-protocol/types"; +import React from "react"; // Default metadata of the story https://storybook.js.org/docs/react/api/csf#default-export const meta: Meta = { diff --git a/packages/ui/src/AppBar/AppBar.stories.tsx b/packages/ui/src/AppBar/AppBar.stories.tsx index 2aa33e77..17d1ab21 100644 --- a/packages/ui/src/AppBar/AppBar.stories.tsx +++ b/packages/ui/src/AppBar/AppBar.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { AppBar } from "./AppBar"; +import React from "react"; // Default metadata of the story https://storybook.js.org/docs/react/api/csf#default-export const meta: Meta = { diff --git a/packages/ui/src/AppBar/AppBar.tsx b/packages/ui/src/AppBar/AppBar.tsx index 57c1289a..2c7ab864 100644 --- a/packages/ui/src/AppBar/AppBar.tsx +++ b/packages/ui/src/AppBar/AppBar.tsx @@ -27,13 +27,18 @@ const BalanceChip = ({ balance }: { balance: number }) => ( sx={{ fontSize: "0.8125rem", lineHeight: "1.125rem", - opacity: 0.6, + opacity: 1, + color: "var(--neutral-300, #D4D4D4)", }} > {balance} XLM } - sx={{ padding: "0.75rem!important" }} + sx={{ + padding: "0.75rem!important", + background: "var(--neutral-800, #262626)", + borderColor: "var(--neutral-700, #404040)", + }} variant="outlined" /> ); @@ -57,16 +62,19 @@ const OptionMenu = ({ return ( - - - + {walletAddress && ( + + + + )} @@ -91,19 +100,35 @@ const OptionMenu = ({ handleClose(); }} > - + Wallet Address - - + + {walletAddress.slice(0, 15)} ... - + { disconnectWallet(); handleClose(); @@ -137,12 +162,12 @@ const AppBar = ({ background: largerThenMd ? "transparent" : "linear-gradient(180deg, #1A1C20 0%, #0E1011 100%)", - position: {xs: "fixed", md: "absolute"}, + position: { xs: "fixed", md: "absolute" }, top: 0, left: 0, width: "100%", p: "0.8rem 0.3rem", - zIndex: 1 + zIndex: 1, }} > - + - {walletAddress && balance >= 0 ? ( - <> - - - - ) : ( + {walletAddress && balance >= 0 && } + + {!walletAddress || balance < 0 ? ( - )} + ) : null} {!largerThenMd && ( diff --git a/packages/ui/src/Button/Button.tsx b/packages/ui/src/Button/Button.tsx index 8285dc60..aadb77d8 100644 --- a/packages/ui/src/Button/Button.tsx +++ b/packages/ui/src/Button/Button.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Button as MuiButton } from "@mui/material"; -import Colors from "../Theme/colors"; import { ButtonProps } from "@phoenix-protocol/types"; +import { colors, borderRadius, typography, shadows } from "../Theme/styleConstants"; const Button = ({ type = "primary", @@ -9,26 +9,42 @@ const Button = ({ label, ...props }: ButtonProps) => { + // Define consistent styling based on our style constants const styles = { wrapper: { display: "inline-block", - background: "linear-gradient(180deg, #E2391B 0%, #E29E1B 100%)", - padding: "1px", // This is the border witdh - borderRadius: "16px", + background: colors.primary.gradient, + padding: "1px", + borderRadius: borderRadius.lg, width: props.fullWidth ? "100%" : "auto", }, button: { - background: type === "primary" ? Colors.primary : Colors.background, + background: + type === "primary" + ? colors.primary.gradient + : colors.gradients.card, border: "none", - padding: - size === "medium" ? "18px 40px 18px 40px" : "12px 40px 12px 40px", - borderRadius: "15px", // Slighter less then the wrapper to show the border - fontSize: "14px", - fontWeight: "700", + padding: size === "medium" ? "14px 32px" : "10px 24px", + borderRadius: borderRadius.lg, + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeights.bold, + fontFamily: typography.fontFamily, lineHeight: "20px", textTransform: "none", - color: Colors.backgroundLight, + color: "#FFFFFF", width: "100%", + boxShadow: + type === "primary" ? shadows.elevated : "none", + transition: "all 0.2s ease-in-out", + "&:hover": { + transform: type === "primary" ? "translateY(-2px)" : "none", + boxShadow: + type === "primary" ? shadows.card : "none", + }, + "&:disabled": { + background: colors.neutral[700], + color: colors.neutral[400], + }, }, }; @@ -39,10 +55,9 @@ const Button = ({ ) || {}), }} {...otherProps} > diff --git a/packages/ui/src/Common/CardContainer.stories.tsx b/packages/ui/src/Common/CardContainer.stories.tsx new file mode 100644 index 00000000..09f14d45 --- /dev/null +++ b/packages/ui/src/Common/CardContainer.stories.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import { StoryFn, Meta } from "@storybook/react"; +import { CardContainer } from "./CardContainer"; +import { Box, Typography } from "@mui/material"; + +export default { + title: "Common/CardContainer", + component: CardContainer, + parameters: { + layout: "centered", + }, +} as Meta; + +const Template: StoryFn = (args) => ( + + + +); + +export const Default = Template.bind({}); +Default.args = { + children: ( + + + Card Title + + + This is a standard card with default styling. It can contain any content. + + + ), +}; + +export const Highlighted = Template.bind({}); +Highlighted.args = { + highlighted: true, + children: ( + + + Highlighted Card + + + This card has special highlight styling to make it stand out. + + + ), +}; + +export const NoPadding = Template.bind({}); +NoPadding.args = { + noPadding: true, + children: ( + + + No Padding Card + + + This card has no built-in padding, allowing for custom content layouts. + + + ), +}; + +export const CustomStyling = Template.bind({}); +CustomStyling.args = { + sx: { + maxWidth: "400px", + background: "linear-gradient(135deg, #262626 0%, #171717 100%)", + boxShadow: "0 8px 16px rgba(0, 0, 0, 0.2)", + }, + children: ( + + + Custom Styled Card + + + This card has custom styles applied via the sx prop, demonstrating how the component can be extended. + + + ), +}; diff --git a/packages/ui/src/Common/CardContainer.tsx b/packages/ui/src/Common/CardContainer.tsx new file mode 100644 index 00000000..62ae9733 --- /dev/null +++ b/packages/ui/src/Common/CardContainer.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { Box, BoxProps } from "@mui/material"; +import { cardStyles } from "../Theme/styleConstants"; + +interface CardContainerProps extends BoxProps { + highlighted?: boolean; + noPadding?: boolean; + children: React.ReactNode; +} + +export const CardContainer = ({ + highlighted = false, + noPadding = false, + children, + sx = {}, + ...props +}: CardContainerProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/ui/src/Common/CustomDropdown.tsx b/packages/ui/src/Common/CustomDropdown.tsx new file mode 100644 index 00000000..f9d10636 --- /dev/null +++ b/packages/ui/src/Common/CustomDropdown.tsx @@ -0,0 +1,220 @@ +import React, { useState, useRef, useMemo } from "react"; +import { + Box, + Typography, + TextField, + Popper, + Paper, + MenuList, + MenuItem, +} from "@mui/material"; + +type Pool = { + tokenA: { icon: string; symbol: string }; + tokenB: { icon: string; symbol: string }; + contractAddress: string; +}; + +interface CustomDropdownProps { + pools: Pool[]; + selectedPoolForVolume: string | undefined; + setSelectedPoolForVolume: (pool: string | undefined) => void; +} + +const CustomDropdown = ({ + pools, + selectedPoolForVolume, + setSelectedPoolForVolume, +}: CustomDropdownProps) => { + const [open, setOpen] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const anchorRef = useRef(null); + const searchRef = useRef(null); + + const filteredPools = useMemo(() => { + if (!searchTerm) return pools; + return pools.filter( + (pool) => + pool.tokenA.symbol.toLowerCase().includes(searchTerm.toLowerCase()) || + pool.tokenB.symbol.toLowerCase().includes(searchTerm.toLowerCase()) + ); + }, [pools, searchTerm]); + + const handleToggle = () => { + setOpen((prevOpen) => !prevOpen); + }; + + const handleClose = (event: any) => { + if ( + anchorRef.current && + anchorRef.current.contains(event.target as HTMLElement) + ) { + return; + } + if (searchRef.current?.contains(event.target)) { + return; // keep it open if the click is inside the search + } + + setOpen(false); + }; + + const handleMenuItemClick = ( + event: React.MouseEvent, + poolAddress: string | undefined + ) => { + setSelectedPoolForVolume(poolAddress); + setOpen(false); + }; + + return ( +
+ + + {selectedPoolForVolume === undefined || + selectedPoolForVolume === "All" + ? "All Pools" + : pools.find((p) => p.contractAddress === selectedPoolForVolume) + ?.tokenA.symbol + + " / " + + pools.find((p) => p.contractAddress === selectedPoolForVolume) + ?.tokenB.symbol} + + + + + e.stopPropagation()} + style={{ + pointerEvents: "none", + paddingTop: 0, + paddingBottom: "0.5rem", + }} + > + { + e.stopPropagation(); + e.nativeEvent.stopImmediatePropagation(); + }} + > + setSearchTerm(e.target.value)} + InputProps={{ + disableUnderline: true, + style: { color: "var(--neutral-300, #D4D4D4)" }, + }} + sx={{ + "& .MuiInputBase-input": { + padding: 0, + }, + }} + /> + + + handleMenuItemClick(event, undefined)} + sx={{ + textAlign: "center", + color: "var(--neutral-300, #D4D4D4)", + }} + > + All + + {filteredPools.map((pool) => ( + + handleMenuItemClick(event, pool.contractAddress) + } + > + + {pool.tokenA.symbol} + + {pool.tokenA.symbol} + + / + {pool.tokenB.symbol} + + {pool.tokenB.symbol} + + + + ))} + + + + +
+ ); +}; + +export { CustomDropdown }; diff --git a/packages/ui/src/Common/CustomTooltip.stories.tsx b/packages/ui/src/Common/CustomTooltip.stories.tsx new file mode 100644 index 00000000..c188c2b5 --- /dev/null +++ b/packages/ui/src/Common/CustomTooltip.stories.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { StoryFn, Meta } from "@storybook/react"; +import { CustomTooltip } from "./CustomTooltip"; +import { Box } from "@mui/material"; + +export default { + title: "Common/CustomTooltip", + component: CustomTooltip, + parameters: { + layout: "centered", + }, +} as Meta; + +const Template: StoryFn = (args) => ( + + + +); + +export const Default = Template.bind({}); +Default.args = { + active: true, + payload: [ + { name: "Volume", value: 1234567 }, + { name: "Transactions", value: 456 }, + ], + label: "January 1, 2023", +}; + +export const SingleValue = Template.bind({}); +SingleValue.args = { + active: true, + payload: [ + { name: "Price", value: 0.0485 }, + ], + label: "Today", +}; + +export const MultipleValues = Template.bind({}); +MultipleValues.args = { + active: true, + payload: [ + { name: "Open", value: 0.0445 }, + { name: "Close", value: 0.0485 }, + { name: "High", value: 0.0492 }, + { name: "Low", value: 0.0438 }, + ], + label: "October 15, 2023", +}; diff --git a/packages/ui/src/Common/CustomTooltip.tsx b/packages/ui/src/Common/CustomTooltip.tsx new file mode 100644 index 00000000..5bc994d3 --- /dev/null +++ b/packages/ui/src/Common/CustomTooltip.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { Box, Typography } from "@mui/material"; +import { commonStyles, colors, typography } from "../Theme/styleConstants"; + +interface CustomTooltipProps { + active?: boolean; + payload?: any[]; + label?: string; +} + +export const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => { + if (!active || !payload || !payload.length) { + return null; + } + + return ( + + {label && ( + + {label} + + )} + + {payload.map((entry, index) => ( + + + {entry.name}: + + + {entry.value.toLocaleString()} + + + ))} + + ); +}; diff --git a/packages/ui/src/Common/SearchInput.stories.tsx b/packages/ui/src/Common/SearchInput.stories.tsx new file mode 100644 index 00000000..543636c8 --- /dev/null +++ b/packages/ui/src/Common/SearchInput.stories.tsx @@ -0,0 +1,43 @@ +import React, { useState } from "react"; +import { StoryFn, Meta } from "@storybook/react"; +import { SearchInput } from "./SearchInput"; +import { Box } from "@mui/material"; + +export default { + title: "Common/SearchInput", + component: SearchInput, + parameters: { + layout: "centered", + }, +} as Meta; + +const Template: StoryFn = (args) => { + const [value, setValue] = useState(args.value || ""); + return ( + + setValue(e.target.value)} + /> + + ); +}; + +export const Default = Template.bind({}); +Default.args = { + placeholder: "Search", + value: "", +}; + +export const WithValue = Template.bind({}); +WithValue.args = { + placeholder: "Search", + value: "Token", +}; + +export const CustomPlaceholder = Template.bind({}); +CustomPlaceholder.args = { + placeholder: "Search by name or address", + value: "", +}; diff --git a/packages/ui/src/Common/SearchInput.tsx b/packages/ui/src/Common/SearchInput.tsx new file mode 100644 index 00000000..7256844e --- /dev/null +++ b/packages/ui/src/Common/SearchInput.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { Box, Input, InputProps } from "@mui/material"; +import { colors, borderRadius, typography, spacing } from "../Theme/styleConstants"; + +interface SearchInputProps extends Omit { + value: string; + onChange: (e: React.ChangeEvent) => void; + placeholder?: string; +} + +export const SearchInput = ({ + value, + onChange, + placeholder = "Search", + sx = {}, + ...props +}: SearchInputProps) => { + return ( + + } + {...props} + /> + ); +}; diff --git a/packages/ui/src/Common/Toast/Toast.stories.tsx b/packages/ui/src/Common/Toast/Toast.stories.tsx new file mode 100644 index 00000000..bb0b53c8 --- /dev/null +++ b/packages/ui/src/Common/Toast/Toast.stories.tsx @@ -0,0 +1,247 @@ +import React from "react"; +import { StoryFn, Meta } from "@storybook/react"; +import { Button, Box, Stack } from "@mui/material"; +import { Toast, ToastProps } from "./Toast"; +import { ToastContainer } from "./ToastContainer"; +import { ToastProvider, useToast } from "./useToast"; + +export default { + title: "Common/Toast", + component: Toast, + parameters: { + layout: "centered", + }, +} as Meta; + +// Individual Toast Story +const SingleToastTemplate: StoryFn = (args) => ( + + + +); + +export const Success = SingleToastTemplate.bind({}); +Success.args = { + id: "1", + type: "success", + title: "Success", + message: "Operation completed successfully!", + onClose: () => console.log("Toast closed"), +}; + +export const Error = SingleToastTemplate.bind({}); +Error.args = { + id: "2", + type: "error", + title: "Error", + message: "Something went wrong. Please try again.", + onClose: () => console.log("Toast closed"), + // Use a plain object instead of Error constructor for storybook compatibility + error: { + message: "Detailed error message", + stack: + "Error: Detailed error message\n at Function.execute (http://localhost:6006/main.iframe.bundle.js:1234:15)\n at onClick (http://localhost:6006/main.iframe.bundle.js:5678:10)", + }, +}; + +export const Warning = SingleToastTemplate.bind({}); +Warning.args = { + id: "3", + type: "warning", + title: "Warning", + message: "This action may have side effects.", + onClose: () => console.log("Toast closed"), +}; + +export const Info = SingleToastTemplate.bind({}); +Info.args = { + id: "4", + type: "info", + title: "Information", + message: "Your transaction is being processed.", + onClose: () => console.log("Toast closed"), +}; + +export const Loading = SingleToastTemplate.bind({}); +Loading.args = { + id: "5", + type: "loading", + title: "Processing", + message: "Please wait while we process your request...", + onClose: () => console.log("Toast closed"), +}; + +export const SuccessWithTransaction = SingleToastTemplate.bind({}); +SuccessWithTransaction.args = { + id: "6", + type: "success", + title: "Transaction Complete", + message: "Your transaction has been processed successfully.", + onClose: () => console.log("Toast closed"), + transactionId: + "3389e9febc1f45efdae5866971360fedb2c53880453d83e8941b7dec5ac8981e", +}; + +// Toast Container with multiple toasts +const ToastContainerExample = () => { + const [toasts, setToasts] = React.useState([ + { + id: "1", + type: "success", + title: "Transaction Complete", + message: "Your transaction has been processed successfully.", + onClose: (id) => + setToasts((prev) => prev.filter((toast) => toast.id !== id)), + }, + { + id: "2", + type: "error", + title: "Connection Error", + message: "Failed to connect to the server. Please check your network.", + onClose: (id) => + setToasts((prev) => prev.filter((toast) => toast.id !== id)), + }, + ]); + + const onClose = (id: string) => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }; + + return ( + + + + ); +}; + +export const ToastContainerStory = () => ; +ToastContainerStory.storyName = "Toast Container"; + +// Toast Provider with interactive demo +const ToastDemo = () => { + const { success, error, warning, info, loading, removeAll, addAsyncToast } = + useToast(); + + const simulateAsyncOperation = () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + success: true, + transactionId: + "3389e9febc1f45efdae5866971360fedb2c53880453d83e8941b7dec5ac8981e", + }); + }, 3000); + }); + }; + + const simulateAsyncError = () => { + return new Promise((_, reject) => { + setTimeout(() => { + // Create an error-like object instead of using the Error constructor + reject({ + message: "Failed to complete the operation", + stack: + "Error: Failed to complete the operation\n at simulateAsyncError\n at onClick\n at handleClick", + }); + }, 3000); + }); + }; + + return ( + + + + + + + + + + + + + ); +}; + +export const ToastSystem = () => ( + + + +); +ToastSystem.storyName = "Toast System"; diff --git a/packages/ui/src/Common/Toast/Toast.tsx b/packages/ui/src/Common/Toast/Toast.tsx new file mode 100644 index 00000000..78b75466 --- /dev/null +++ b/packages/ui/src/Common/Toast/Toast.tsx @@ -0,0 +1,283 @@ +import React, { useState } from "react"; +import { + Box, + IconButton, + Typography, + CircularProgress, + Link, + Collapse, +} from "@mui/material"; +import { motion } from "framer-motion"; +import CloseIcon from "@mui/icons-material/Close"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import ErrorIcon from "@mui/icons-material/Error"; +import InfoIcon from "@mui/icons-material/Info"; +import WarningIcon from "@mui/icons-material/Warning"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import ExpandLessIcon from "@mui/icons-material/ExpandLess"; +import LaunchIcon from "@mui/icons-material/Launch"; +import { + colors, + typography, + spacing, + borderRadius, + shadows, +} from "../../Theme/styleConstants"; + +export type ToastType = "success" | "error" | "info" | "warning" | "loading"; + +export interface ToastProps { + id: string; + type: ToastType; + message: string; + title?: string; + onClose: (id: string) => void; + autoHideDuration?: number; + transactionId?: string; + error?: Error | string | { message: string; stack?: string }; // For collapsible error details +} + +const getToastIcon = (type: ToastType) => { + switch (type) { + case "success": + return ; + case "error": + return ; + case "warning": + return ; + case "info": + return ; + case "loading": + return ; + default: + return ; + } +}; + +const getToastColor = (type: ToastType) => { + switch (type) { + case "success": + return colors.success.main; + case "error": + return colors.error.main; + case "warning": + return colors.warning.main; + case "loading": + return colors.info.main; + case "info": + default: + return colors.primary.main; + } +}; + +// Helper function to get Explorer URL for transaction +const getExplorerUrl = (transactionId: string) => { + return `https://stellar.expert/explorer/public/tx/${transactionId}`; +}; + +export const Toast = ({ + id, + type, + title, + message, + onClose, + autoHideDuration = 5000, + transactionId, + error, +}: ToastProps) => { + const [expanded, setExpanded] = useState(false); + + React.useEffect(() => { + // Don't auto-hide loading toasts + if (autoHideDuration && type !== "loading") { + const timer = setTimeout(() => { + onClose(id); + }, autoHideDuration); + + return () => { + clearTimeout(timer); + }; + } + }, [id, onClose, autoHideDuration, type]); + + // Format error details for display + const errorMessage = React.useMemo(() => { + if (!error) return ""; + if (typeof error === "string") return error; + + // Handle Error objects and plain objects with message/stack properties + const errorObj = error as any; + return errorObj.stack || errorObj.message || String(error); + }, [error]); + + const hasErrorDetails = type === "error" && errorMessage; + + return ( + + + + + {type === "loading" ? ( + + ) : ( + getToastIcon(type) + )} + + + + {title && ( + + {title} + + )} + + + {message} + + + {/* Transaction Link */} + {transactionId && ( + + View Transaction + + + )} + + {/* Error expand/collapse button */} + {hasErrorDetails && ( + setExpanded(!expanded)} + sx={{ + display: "flex", + alignItems: "center", + mt: 1, + cursor: "pointer", + fontSize: typography.fontSize.xs, + color: colors.primary.main, + }} + > + + {expanded ? "Hide Details" : "Show Details"} + + {expanded ? ( + + ) : ( + + )} + + )} + + + {/* Only show close button for non-loading toasts */} + {type !== "loading" && ( + onClose(id)} + sx={{ + padding: "4px", + color: colors.neutral[400], + "&:hover": { + color: colors.neutral[200], + backgroundColor: "transparent", + }, + }} + > + + + )} + + + {/* Collapsible Error Details */} + {hasErrorDetails && ( + + + {errorMessage} + + + )} + + + ); +}; diff --git a/packages/ui/src/Common/Toast/ToastContainer.tsx b/packages/ui/src/Common/Toast/ToastContainer.tsx new file mode 100644 index 00000000..b2a9a70c --- /dev/null +++ b/packages/ui/src/Common/Toast/ToastContainer.tsx @@ -0,0 +1,85 @@ +import React, { useContext } from "react"; +import { Box } from "@mui/material"; +import { AnimatePresence } from "framer-motion"; +import { Toast, ToastProps } from "./Toast"; +import { spacing } from "../../Theme/styleConstants"; +import { useToast } from "./useToast"; + +interface ToastContainerProps { + toasts?: ToastProps[]; + position?: + | "top-right" + | "top-left" + | "bottom-right" + | "bottom-left" + | "top-center" + | "bottom-center"; + onClose?: (id: string) => void; +} + +export const ToastContainer = ({ + toasts: externalToasts, + position = "bottom-right", + onClose: externalOnClose, +}: ToastContainerProps) => { + // Get toasts from context if not provided externally + const toastContext = useToast(); + const toasts = externalToasts || toastContext.toasts; + const onClose = externalOnClose || toastContext.removeToast; + + // Define position styles + const getPositionStyle = () => { + switch (position) { + case "top-right": + return { top: spacing.md, right: spacing.md }; + case "top-left": + return { top: spacing.md, left: spacing.md }; + case "bottom-left": + return { bottom: spacing.md, left: spacing.md }; + case "top-center": + return { top: spacing.md, left: "50%", transform: "translateX(-50%)" }; + case "bottom-center": + return { + bottom: spacing.md, + left: "50%", + transform: "translateX(-50%)", + }; + case "bottom-right": + default: + return { bottom: spacing.md, right: spacing.md }; + } + }; + + return ( + + + {toasts.map((toast) => ( + + ))} + + + ); +}; diff --git a/packages/ui/src/Common/Toast/index.ts b/packages/ui/src/Common/Toast/index.ts new file mode 100644 index 00000000..b623da7b --- /dev/null +++ b/packages/ui/src/Common/Toast/index.ts @@ -0,0 +1,3 @@ +export * from './Toast'; +export * from './ToastContainer'; +export * from './useToast'; diff --git a/packages/ui/src/Common/Toast/useToast.tsx b/packages/ui/src/Common/Toast/useToast.tsx new file mode 100644 index 00000000..2dbd4d04 --- /dev/null +++ b/packages/ui/src/Common/Toast/useToast.tsx @@ -0,0 +1,177 @@ +import React, { useState, useCallback, useContext, createContext } from "react"; +import { v4 as uuidv4 } from "uuid"; +import { ToastProps, ToastType } from "./Toast"; + +interface ToastContextType { + toasts: ToastProps[]; + addToast: (options: Omit) => string; + removeToast: (id: string) => void; + removeAll: () => void; + success: ( + message: string, + options?: Partial> + ) => string; + error: ( + message: string, + options?: Partial> + ) => string; + warning: ( + message: string, + options?: Partial> + ) => string; + info: ( + message: string, + options?: Partial> + ) => string; + loading: ( + message: string, + options?: Partial> + ) => string; + addAsyncToast: ( + promise: Promise, + loadingMessage: string, + options?: Partial> + ) => Promise; + updateToast: ( + id: string, + options: Partial> + ) => void; +} + +const ToastContext = createContext(undefined); + +export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [toasts, setToasts] = useState([]); + + const removeToast = useCallback((id: string) => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }, []); + + const addToast = useCallback( + (options: Omit) => { + const id = uuidv4(); + setToasts((prev) => [...prev, { ...options, id, onClose: removeToast }]); + return id; + }, + [removeToast] + ); + + const updateToast = useCallback( + (id: string, options: Partial>) => { + setToasts((prev) => + prev.map((toast) => + toast.id === id ? { ...toast, ...options } : toast + ) + ); + }, + [] + ); + + const removeAll = useCallback(() => { + setToasts([]); + }, []); + + const createToastWithType = useCallback( + (type: ToastType) => + ( + message: string, + options?: Partial< + Omit + > + ) => { + return addToast({ message, type, ...options }); + }, + [addToast] + ); + + // Add async toast handling + const addAsyncToast = useCallback( + async ( + promise: Promise, + loadingMessage: string, + options?: Partial> + ) => { + const toastId = addToast({ + message: loadingMessage, + type: "loading", + ...options, + }); + + try { + const result = await promise; + + // Extract transaction ID if available in the result + const transactionId = result?.transactionId || undefined; + + updateToast(toastId, { + type: "success", + message: options?.title + ? `${options.title} succeeded` + : "Operation completed successfully!", + transactionId, + }); + return result; + } catch (error) { + // Create a serializable error object + const errorMessage = + error instanceof Error + ? error.message + : typeof error === "object" && error !== null && "message" in error + ? error.message + : "Operation failed"; + + const errorObj = + error instanceof Error + ? { message: error.message, stack: error.stack } + : typeof error === "object" && error !== null + ? error + : { message: String(error) }; + + updateToast(toastId, { + type: "error", + message: errorMessage as string, + error: errorObj as + | string + | Error + | { message: string; stack?: string | undefined } + | undefined, // Store a serializable error object + }); + throw error; + } finally { + // Auto remove the toast after 5 seconds + setTimeout(() => { + removeToast(toastId); + }, 5000); + } + }, + [addToast, updateToast, removeToast] + ); + + const value = { + toasts, + addToast, + removeToast, + removeAll, + updateToast, + addAsyncToast, + success: createToastWithType("success"), + error: createToastWithType("error"), + warning: createToastWithType("warning"), + info: createToastWithType("info"), + loading: createToastWithType("loading"), + }; + + return ( + {children} + ); +}; + +export const useToast = (): ToastContextType => { + const context = useContext(ToastContext); + if (context === undefined) { + throw new Error("useToast must be used within a ToastProvider"); + } + return context; +}; diff --git a/packages/ui/src/ConnectWallet/Carousel.tsx b/packages/ui/src/ConnectWallet/Carousel.tsx index e8ed19b7..bdcd7dd5 100644 --- a/packages/ui/src/ConnectWallet/Carousel.tsx +++ b/packages/ui/src/ConnectWallet/Carousel.tsx @@ -1,122 +1,212 @@ import React, { useState } from "react"; import { Box, Typography, IconButton } from "@mui/material"; +import { motion, AnimatePresence } from "framer-motion"; +import { colors, typography, spacing, borderRadius } from "../Theme/styleConstants"; import { ArrowBackIos, ArrowForwardIos } from "@mui/icons-material"; -import { motion } from "framer-motion"; -import { display } from "@mui/system"; interface CarouselItem { - image: string; - text: string; title: string; + content: string; + image: string; } interface CarouselProps { items: CarouselItem[]; } -export const Carousel = ({ items }: CarouselProps) => { +const CarouselComponent = ({ items }: CarouselProps) => { const [currentIndex, setCurrentIndex] = useState(0); + const [direction, setDirection] = useState(0); + + const handleNext = () => { + setDirection(1); + setCurrentIndex((prevIndex) => (prevIndex + 1) % items.length); + }; const handlePrev = () => { - setCurrentIndex((prevIndex) => - prevIndex === 0 ? items.length - 1 : prevIndex - 1 - ); + setDirection(-1); + setCurrentIndex((prevIndex) => (prevIndex - 1 + items.length) % items.length); }; - const handleNext = () => { - setCurrentIndex((prevIndex) => - prevIndex === items.length - 1 ? 0 : prevIndex + 1 - ); + const handleDotClick = (index: number) => { + setDirection(index > currentIndex ? 1 : -1); + setCurrentIndex(index); + }; + + const variants = { + enter: (direction: number) => ({ + x: direction > 0 ? 200 : -200, + opacity: 0, + }), + center: { + x: 0, + opacity: 1, + }, + exit: (direction: number) => ({ + x: direction < 0 ? 200 : -200, + opacity: 0, + }), }; return ( - - - - + + + + + + {/* If the image source exists, use it, otherwise display a placeholder */} + {items[currentIndex].image ? ( + { + // Fallback if image fails to load + e.currentTarget.src = "/placeholder-wallet.svg"; + }} + /> + ) : ( + + + ? + + + )} + + {items[currentIndex].title} + + + {items[currentIndex].content} + + + + + + + {/* Navigation dots */} - + {items.map((_, index) => ( - - {items[currentIndex].title} - - handleDotClick(index)} sx={{ - fontSize: 14, - opacity: 0.4, - marginBottom: "1rem", - textAlign: "center", + width: 10, + height: 10, + borderRadius: "50%", + backgroundColor: index === currentIndex ? colors.primary.main : colors.neutral[700], + cursor: "pointer", + transition: "all 0.3s ease", + "&:hover": { + backgroundColor: index === currentIndex ? colors.primary.main : colors.neutral[600], + }, }} - > - {items[currentIndex].text} - - + /> + ))} + + {/* Navigation arrows */} + + + - + - - {items.map((_, index) => ( - - ))} - ); }; + +export default CarouselComponent; diff --git a/packages/ui/src/ConnectWallet/ConnectWallet.tsx b/packages/ui/src/ConnectWallet/ConnectWallet.tsx index 2446e586..6a517547 100644 --- a/packages/ui/src/ConnectWallet/ConnectWallet.tsx +++ b/packages/ui/src/ConnectWallet/ConnectWallet.tsx @@ -4,18 +4,27 @@ import { ConnectWalletProps, OptionComponentProps, } from "@phoenix-protocol/types"; -import { Box, Modal, Typography, Grid, Skeleton } from "@mui/material"; -import { motion } from "framer-motion"; -import { Button as PhoenixButton } from "../Button/Button"; -import Colors from "../Theme/colors"; -import { Carousel } from "./Carousel"; // Correct the import path +import { + Box, + Modal, + Typography, + Grid, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { motion, AnimatePresence } from "framer-motion"; +import { Button } from "../Button/Button"; +import { + colors, + typography, + spacing, + borderRadius, +} from "../Theme/styleConstants"; +import CarouselComponent from "./Carousel"; +import CloseIcon from "@mui/icons-material/Close"; /** - * OptionComponent - * Renders an individual wallet option with hover effects and selection state. - * - * @param {OptionComponentProps} props - Contains wallet connector, click handler, and selection status. - * @returns {JSX.Element} The wallet option component. + * OptionComponent for displaying individual wallet options */ const OptionComponent = ({ connector, @@ -23,80 +32,187 @@ const OptionComponent = ({ selected, allowed, }: OptionComponentProps & { allowed: boolean }) => { - const hoverStyles = { - background: - "linear-gradient(137deg, rgba(226, 73, 26, 0.20) 0%, rgba(226, 27, 27, 0.20) 17.08%, rgba(226, 73, 26, 0.20) 42.71%, rgba(226, 170, 27, 0.20) 100%)", - cursor: "pointer", - border: "2px solid #E2621B", - transition: "all 0.2s ease-in-out", - }; - - const baseStyles = { - display: "flex", - flexDirection: { xs: "column", md: "row" }, - padding: { md: "1.125rem 1.5rem", xs: "1rem" }, - alignItems: "center", - gap: "0.25rem", - background: - "linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.03) 100%)", - border: "2px solid transparent", - width: { xs: "auto", md: "100%" }, - marginTop: "1.25rem", - borderRadius: "8px", - transition: "all 0.2s ease-in-out", - opacity: allowed ? 1 : 0.5, - marginRight: { xs: "1rem", md: 0 }, - "&:hover": hoverStyles, - }; - return ( - + - {connector.name} - - {connector.name} - - {!allowed && ( - Not installed + {connector.name} - )} + {!allowed && ( + + Not installed + + )} + ); }; /** - * ConnectWallet - * Modal to display wallet connection options and handle the connection process. - * - * @param {ConnectWalletProps} props - Props for managing modal state, connectors, and the connect function. - * @returns {JSX.Element} The ConnectWallet modal component. + * Loading screen when connecting to a wallet + */ +const WalletConnectingScreen = ({ connector, onBack }) => ( + + + + + Opening {connector?.name} + + + Please confirm the connection request in the {connector?.name} app + + + + +); + +/** + * Information slides about crypto wallets + */ +const InfoSlides = () => { + const items = [ + { + title: "What are wallets?", + content: + "Wallets are used to send, receive, and access all your digital assets like PHO and XLM.", + image: "/wallet-3.png", + }, + { + title: "No accounts. No passwords.", + content: + "Use your wallet to sign into many different platforms. No unique accounts or passwords.", + image: "/wallet-2.png", + }, + { + title: "Your wallet, your keys.", + content: + "Your wallet is your key to the Stellar network. Keep it safe and secure. Phoenix Protocol never has access to your funds.", + image: "/wallet-1.png", + }, + ]; + + return ( + + + + ); +}; + +/** + * Main ConnectWallet component */ const ConnectWallet = ({ open, @@ -104,6 +220,9 @@ const ConnectWallet = ({ connectors, connect, }: ConnectWalletProps): JSX.Element => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const [loading, setLoading] = useState(false); const [selected, setSelected] = useState(undefined); const [allowedConnectors, setAllowedConnectors] = useState([]); @@ -112,307 +231,258 @@ const ConnectWallet = ({ ); const [loadingConnectors, setLoadingConnectors] = useState(true); + // Check which connectors are allowed (installed) useEffect(() => { const checkConnectors = async () => { setLoadingConnectors(true); const allowed: Connector[] = []; const disallowed: Connector[] = []; + for (const connector of connectors) { - const isAllowed = await connector.isConnected(); - if (isAllowed) { - allowed.push(connector); - } else { + try { + const isAllowed = await connector.isConnected(); + if (isAllowed) { + allowed.push(connector); + } else { + disallowed.push(connector); + } + } catch (error) { disallowed.push(connector); } } + setAllowedConnectors(allowed); setDisallowedConnectors(disallowed); setLoadingConnectors(false); }; - checkConnectors(); - }, [connectors]); - /** - * Handles wallet connection. - * Initiates the connection process for the selected connector. - * - * @param {Connector} connector - The wallet connector to connect with. - */ + if (open) { + checkConnectors(); + } + }, [connectors, open]); + + // Handle connecting to a wallet const handleConnect = useCallback( async (connector: Connector) => { setLoading(true); + setSelected(connector); + try { await connect(connector); + // Success will be handled by the parent component closing the modal } catch (error) { - console.log("Wallet connection failed:", error); - } finally { + console.error("Wallet connection failed:", error); setLoading(false); - setOpen(false); } }, - [connect, setOpen] + [connect] ); - /** - * Handles the "Back" button action during the loading state. - */ + // Handle going back from the loading screen const handleBack = useCallback(() => { setLoading(false); setSelected(undefined); }, []); - const modalStyle = { - position: "absolute" as "absolute", - alignItems: { xs: "center", md: "flex-start" }, - top: { md: "50%", xs: "0" }, - left: "50%", - transform: { md: "translate(-50%, -50%)", xs: "translate(-50%, 0)" }, - width: { xs: "96vh", md: 800 }, - maxWidth: "calc(100vw - 16px)", - background: "linear-gradient(180deg, #292B2C 0%, #1F2123 100%)", - borderRadius: "16px", - flexDirection: { xs: "row", md: "column" }, - minHeight: "50vh", - maxHeight: { md: "530px", xs: "100vh" }, - }; - - const carouselItems = [ - { - image: "/pho-wallets.png", - title: "What are wallets?", - text: "Wallets are used to send, receive, and access all your digital assets like PHO and XLM.", - }, - { - image: "/pho-wallets.png", - title: "No accounts. No passwords.", - text: "Use your wallet to sign into many different platforms. No unique accounts or passwords.", - }, - { - image: "/pho-wallets.png", - title: "Your wallet, your keys.", - text: "Your wallet is your key to the Stellar network. Keep it safe and secure. Phoenix Protocol never has access to your funds.", - }, - ]; + // Handle closing the modal + const handleClose = useCallback(() => { + setOpen(false); - const skeletonStyles = { - display: "flex", - padding: { md: "1.125rem 1.5rem", xs: "1rem" }, - alignItems: "center", - gap: "0.25rem", - width: "100%", - marginTop: "1.25rem", - borderRadius: "8px", - }; + // Reset state after animation + setTimeout(() => { + setLoading(false); + setSelected(undefined); + }, 300); + }, [setOpen]); return ( setOpen(false)} - aria-labelledby="connectwallet-modal" - aria-describedby="connect your wallet to the app" + onClose={handleClose} + aria-labelledby="connect-wallet-modal" + aria-describedby="select a wallet to connect to the app" > - - - + - Connect Wallet - - {!loadingConnectors ? ( - <> - - Start by connecting with one of the wallets below. - - + {/* Left Side - Wallet Selection */} + - {allowedConnectors.map((connector) => ( - { - setSelected(connector); - handleConnect(connector); - }} - allowed={true} - /> - ))} - {disallowedConnectors.map((connector) => ( - setSelected(connector)} - allowed={false} - /> - ))} - - - ) : ( - - - - - - )} - - - {loading ? ( - - - Loading Wallet - - Opening {selected?.name} - - - Please confirm in the {selected?.name} app - - - Back - - - - ) : ( - <> - setOpen(false)} > - Close + + Connect Wallet + + + + + + - - + Start by connecting with one of the wallets below. +
+ + {loadingConnectors ? ( + // Loading skeleton + [...Array(4)].map((_, index) => ( + + )) + ) : ( + // Wallet options + + {allowedConnectors.map((connector) => ( + handleConnect(connector)} + allowed={true} + /> + ))} + {disallowedConnectors.map((connector) => ( + {}} + allowed={false} + /> + ))} + + )} + + + {/* Right Side - Loading or Info */} + - - Getting Started - - - - - )} - - + + {loading ? ( + + ) : ( + + + + )} + + + + + + ); }; diff --git a/packages/ui/src/ConnectWallet/index.ts b/packages/ui/src/ConnectWallet/index.ts new file mode 100644 index 00000000..59af7d91 --- /dev/null +++ b/packages/ui/src/ConnectWallet/index.ts @@ -0,0 +1 @@ +export * from './ConnectWallet'; diff --git a/packages/ui/src/Dashboard/AssetInfoModal/AssetInfoModal.stories.tsx b/packages/ui/src/Dashboard/AssetInfoModal/AssetInfoModal.stories.tsx index 337b1479..10745a0e 100644 --- a/packages/ui/src/Dashboard/AssetInfoModal/AssetInfoModal.stories.tsx +++ b/packages/ui/src/Dashboard/AssetInfoModal/AssetInfoModal.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { AssetInfoModal } from "./AssetInfoModal"; +import React from "react"; // Default metadata of the story https://storybook.js.org/docs/react/api/csf#default-export const meta: Meta = { @@ -18,42 +19,342 @@ export const Primary: Story = { open: true, asset: { asset: "PHO-GAX5TXB5RYJNLBUR477PEXM4X75APK2PGMTN6KEFQSESGWFXEAKFSXJO-1", - supply: 2000897775221742, - traded_amount: 6831936130, - payments_amount: 2330868044789, + supply: 1999999999396679, + traded_amount: 4676470585841, + payments_amount: 1603536744556300, created: 1715112013, - trustlines: [178, 178, 87], - payments: 2336, + trustlines: [778, 778, 387], + payments: 31107, domain: "app.phoenix-hub.io", + price7d: [ + [1743085960000, 0.4588211242415678], + [1743138284000, 0.45440554107613923], + [1743138284000, 0.4541021770839637], + [1743141663000, 0.4522770860874685], + [1743141663000, 0.4517545026412518], + [1743148140000, 0.4504794609610309], + [1743148140000, 0.4496975722820581], + [1743159874000, 0.4486984217435167], + [1743159874000, 0.4487893571304989], + [1743170975000, 0.4473196140525047], + [1743170975000, 0.4475531523371983], + [1743181108000, 0.4475531523371983], + [1743181162000, 0.4475531523371983], + [1743190464000, 0.4475531523371983], + [1743190881000, 0.4475531523371983], + [1743190910000, 0.4475531523371983], + [1743191293000, 0.4475531523371983], + [1743191964000, 0.4475531523371983], + [1743192478000, 0.4475531523371983], + [1743193039000, 0.4475531523371983], + [1743193444000, 0.4475531523371983], + [1743212252000, 0.4475531523371983], + [1743217009000, 0.4490103640309041], + [1743217009000, 0.44371903601610674], + [1743217383000, 0.4427173717849195], + [1743217383000, 0.4496861283458413], + [1743232304000, 0.440772156398865], + [1743232304000, 0.4406536924168718], + [1743234011000, 0.43988739518320846], + [1743234574000, 0.4389411046343642], + [1743234574000, 0.4386045593174841], + [1743241505000, 0.43784303711923817], + [1743241505000, 0.4380228752497139], + [1743246206000, 0.4380228752497139], + [1743249134000, 0.4380228752497139], + [1743257524000, 0.4380228752497139], + [1743291521000, 0.4380228752497139], + [1743341099000, 0.43802287524971395], + [1743341156000, 0.43802287524971395], + [1743341237000, 0.4380228752497139], + [1743354692000, 0.4380228752497139], + [1743372432000, 0.43375530701469767], + [1743372432000, 0.43334549751463436], + [1743372537000, 0.43188755747392515], + [1743372537000, 0.4324094533383021], + [1743374084000, 0.4324094533383021], + [1743381259000, 0.43132602644580337], + [1743381259000, 0.43066708975280954], + [1743396635000, 0.43132602644580337], + [1743405591000, 0.42834270041682915], + [1743405591000, 0.42943905958599493], + [1743405990000, 0.42857553014205124], + [1743405990000, 0.42892628399495364], + [1743407554000, 0.427700631919887], + [1743407554000, 0.4269435967040356], + [1743473644000, 0.427700631919887], + [1743480085000, 0.427700631919887], + [1743484922000, 0.427700631919887], + [1743485007000, 0.427700631919887], + [1743498929000, 0.427700631919887], + [1743499467000, 0.427700631919887], + [1743499530000, 0.427700631919887], + [1743499579000, 0.427700631919887], + [1743499619000, 0.427700631919887], + [1743499719000, 0.427700631919887], + [1743499736000, 0.427700631919887], + [1743499753000, 0.427700631919887], + [1743499777000, 0.427700631919887], + [1743501905000, 0.427700631919887], + [1743502007000, 0.427700631919887], + [1743504824000, 0.42481723525264914], + [1743504824000, 0.42533863040297626], + [1743504906000, 0.42481723525264914], + [1743505168000, 0.42515116790320806], + [1743505168000, 0.4237340692761448], + [1743508006000, 0.4237340692761448], + [1743508012000, 0.4243119315452311], + [1743508012000, 0.4229090503753681], + [1743508012000, 0.4222948467034411], + [1743512657000, 0.4222948467034411], + [1743512699000, 0.4222948467034411], + [1743512728000, 0.42229484670344114], + [1743512750000, 0.4222948467034411], + [1743516257000, 0.42086539857962185], + [1743516257000, 0.4234304584539419], + [1743516664000, 0.4198384060297612], + [1743516664000, 0.4241193335953195], + [1743517300000, 0.418839740573239], + [1743517300000, 0.42479305879823587], + [1743517380000, 0.418839740573239], + [1743518138000, 0.41883974057323897], + [1743518196000, 0.418839740573239], + [1743519859000, 0.41883974057323897], + [1743538152000, 0.418839740573239], + [1743541192000, 0.418839740573239], + [1743541260000, 0.418839740573239], + [1743541887000, 0.418839740573239], + [1743549205000, 0.41796309332289594], + [1743549650000, 0.41969094897558906], + [1743550436000, 0.418839740573239], + [1743556565000, 0.418839740573239], + [1743556616000, 0.418839740573239], + [1743556993000, 0.418839740573239], + [1743557280000, 0.41883974057323897], + [1743557400000, 0.418839740573239], + [1743557590000, 0.4188397405732391], + [1743557643000, 0.418839740573239], + [1743558668000, 0.4188397405732391], + [1743559603000, 0.41714861734736974], + [1743559603000, 0.4169852629345386], + [1743565576000, 0.41714861734736974], + [1743576529000, 0.4140740458876916], + [1743576529000, 0.41360460495862517], + [1743577581000, 0.41269498260012083], + [1743577581000, 0.4132819533005161], + [1743579268000, 0.4132819533005161], + [1743586373000, 0.4132819533005161], + [1743586947000, 0.4132819533005161], + [1743592638000, 0.41968389645268017], + [1743595242000, 0.4132819533005161], + [1743596739000, 0.4132819533005161], + [1743597183000, 0.4132819533005161], + [1743631930000, 0.4103410172920976], + [1743631930000, 0.4106175862252827], + [1743633248000, 0.4054340532544669], + [1743633248000, 0.40488003101492587], + [1743633754000, 0.40495469411249907], + [1743633754000, 0.40464524271860924], + [1743634926000, 0.40434426978453464], + [1743634926000, 0.40303395386645186], + [1743640242000, 0.4142650585446133], + [1743640261000, 0.41212991796483245], + [1743642941000, 0.4154868867922161], + [1743648245000, 0.4142650585446133], + [1743648746000, 0.4142650585446133], + [1743683329000, 0.4090945255751361], + [1743683329000, 0.40875021757938745], + ], + volume7d: 13196194700, rating: { - age: 0, - trades: 4, - payments: 4, - trustlines: 3, + age: 6, + trades: 7, + payments: 5, + trustlines: 4, volume7d: 7, interop: 4, - liquidity: 1, - average: 3.3, + liquidity: 2, + average: 5, }, - price7d: [ - [1715126400, 4.918545635483871], - [1715212800, 1.493419405263158], - [1715299200, 1.7245551875], - [1715385600, 2.519457714285714], - [1715472000, 52.90474066666667], - [1715558400, 2.6065395625], - [1715644800, 2.2936092857142856], - ], - volume7d: 43761816818, tomlInfo: { code: "PHO", issuer: "GAX5TXB5RYJNLBUR477PEXM4X75APK2PGMTN6KEFQSESGWFXEAKFSXJO", - image: "/cryptoIcons/pho.svg", + image: + "https://stellar.myfilebase.com/ipfs/QmPqH2eYHpEcMNSgXYhxagt5LvNntm38wKMUUcGwzzG9qt", decimals: 7, orgName: "Phoenix Protocol Group", - orgLogo: "/cryptoIcons/pho.svg", + orgLogo: + "https://stellar.myfilebase.com/ipfs/QmPqH2eYHpEcMNSgXYhxagt5LvNntm38wKMUUcGwzzG9qt", }, + // @ts-ignore + score: 5.969612342362173, paging_token: 1, }, + tradingVolume7d: [ + { + date: { + day: 27, + month: 3, + year: 2025, + }, + tokenAVolume: "83019829", + tokenBVolume: "131460888", + usdVolume: 3.8091251276122713, + }, + { + date: { + day: 28, + month: 3, + year: 2025, + }, + tokenAVolume: "8343233715", + tokenBVolume: "7138146024", + usdVolume: 310.78726893678584, + }, + { + date: { + day: 29, + month: 3, + year: 2025, + }, + tokenAVolume: "10121051852", + tokenBVolume: "9358239140", + usdVolume: 372.6046178365335, + }, + { + date: { + day: 30, + month: 3, + year: 2025, + }, + tokenAVolume: "5038132716", + tokenBVolume: "5553772553", + usdVolume: 193.87095834153993, + }, + { + date: { + day: 31, + month: 3, + year: 2025, + }, + tokenAVolume: "5368555413", + tokenBVolume: "3391688215", + usdVolume: 179.6781933331256, + }, + { + date: { + day: 1, + month: 4, + year: 2025, + }, + tokenAVolume: "33800044637", + tokenBVolume: "37549025192", + usdVolume: 1230.206597583481, + }, + { + date: { + day: 2, + month: 4, + year: 2025, + }, + tokenAVolume: "11115758214", + tokenBVolume: "8806695209", + usdVolume: 377.9764250510451, + }, + { + date: { + day: 3, + month: 4, + year: 2025, + }, + tokenAVolume: "7482465348", + tokenBVolume: "7419093994", + usdVolume: 313.94371234458447, + }, + ], + userBalance: 0, + pools: [ + { + tokens: [ + { + name: "XLM", + icon: "/cryptoIcons/xlm.svg", + amount: 101269.5131775, + category: "", + usdValue: 0, + // @ts-ignore + address: "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA", + }, + { + name: "PHO", + icon: "/cryptoIcons/pho.svg", + amount: 64151.8168754, + category: "", + usdValue: 0, + // @ts-ignore + address: "CBZ7M5B3Y4WWBZ5XK5UZCAFOEZ23KSSZXYECYX3IXM6E2JOLQC52DK32", + }, + ], + // @ts-ignore + tvl: 55456.95296774006, + maxApr: "139.62", + userLiquidity: 0, + poolAddress: "CBCZGGNOEUZG4CAAE7TGTQQHETZMKUT4OIPFHHPKEUX46U4KXBBZ3GLH", + }, + { + tokens: [ + { + name: "PHO", + icon: "/cryptoIcons/pho.svg", + amount: 42983.8944, + category: "", + usdValue: 0, + // @ts-ignore + + address: "CBZ7M5B3Y4WWBZ5XK5UZCAFOEZ23KSSZXYECYX3IXM6E2JOLQC52DK32", + }, + { + name: "USDC", + icon: "/cryptoIcons/usdc.svg", + amount: 17745.4097827, + category: "", + usdValue: 0, + // @ts-ignore + address: "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75", + }, + ], + // @ts-ignore + + tvl: 37567.585156540576, + maxApr: "152.17", + userLiquidity: 0, + poolAddress: "CD5XNKK3B6BEF2N7ULNHHGAMOKZ7P6456BFNIHRF4WNTEDKBRWAE7IAA", + }, + ], + loading: false, + }, +}; + +export const NoUserBalance: Story = { + args: { + ...Primary.args, + userBalance: 0, + }, +}; + +export const NoPools: Story = { + args: { + ...Primary.args, + pools: [], + }, +}; + +export const NoRating: Story = { + args: { + ...Primary.args, + asset: { + ...Primary.args.asset, + rating: null, + }, }, }; diff --git a/packages/ui/src/Dashboard/AssetInfoModal/AssetInfoModal.tsx b/packages/ui/src/Dashboard/AssetInfoModal/AssetInfoModal.tsx index f65bf1f0..87d09613 100644 --- a/packages/ui/src/Dashboard/AssetInfoModal/AssetInfoModal.tsx +++ b/packages/ui/src/Dashboard/AssetInfoModal/AssetInfoModal.tsx @@ -1,220 +1,1011 @@ -import React, { useCallback } from "react"; +import React, { useState, useCallback } from "react"; import { Box, Typography, Modal as MuiModal, Avatar, - Table, - TableBody, - TableRow, - TableCell, + Tabs, + Tab, + Grid, IconButton, + useMediaQuery, + useTheme, + Tooltip, + CircularProgress, } from "@mui/material"; +import { motion, AnimatePresence } from "framer-motion"; import CloseIcon from "@mui/icons-material/Close"; -import Colors from "../../Theme/colors"; -import { AssetInfoModalProps } from "@phoenix-protocol/types"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; +import { AssetInfoModalProps, PoolsFilter } from "@phoenix-protocol/types"; +import { + colors, + typography, + spacing, + borderRadius, + shadows, +} from "../../Theme/styleConstants"; +import { + XAxis, + YAxis, + Tooltip as RechartsTooltip, + ResponsiveContainer, + AreaChart, + Area, + BarChart, + Bar, +} from "recharts"; +import { CardContainer } from "../../Common/CardContainer"; +import { TradingVolume } from "@phoenix-protocol/utils/build/trade_api"; +import { PoolItem } from "../../Pools/Pools"; +import { formatCurrencyStatic } from "@phoenix-protocol/utils"; + +// Interface for a tab panel +interface TabPanelProps { + children?: React.ReactNode; + value: number; + index: number; +} + +// Define chart data type +interface ChartDataPoint { + date: string; + value?: number; + volume?: number; +} + +// Helper function to determine color based on rating +const getRatingColor = (rating: number): string => { + if (rating >= 4) return colors.success[500]; + if (rating >= 2.5) return colors.warning[500]; + return colors.error[500]; +}; + +// Helper function to format large numbers +const formatNumber = (num: number): string => { + if (num >= 1000000) return (num / 1000000).toFixed(1) + "M"; + if (num >= 1000) return (num / 1000).toFixed(1) + "K"; + return num.toFixed(1); +}; + +// Calculate total volume +const calculateTotalVolume = (volumes: TradingVolume[]): number => { + return volumes.reduce((total, volume) => { + return total + Number(volume.usdVolume); + }, 0); +}; + +// Helper function to shorten addresses +const shortenAddress = (address: string): string => { + if (!address) return ""; + return `${address.slice(0, 8)}...${address.slice(-8)}`; +}; + +// Data transformations for charts +const prepareVolumeChartData = ( + data: TradingVolume[] | undefined +): ChartDataPoint[] => { + if (!data || !Array.isArray(data)) return []; + + // Map to date and volume minding the date and time object format + return data.map((item) => { + const date = item.date + ? `${item.date.year}-${String(item.date.month).padStart(2, "0")}-${String( + item.date.day + ).padStart(2, "0")}` + : ""; + + return { + date, + volume: Number(item.usdVolume), + }; + }); +}; + +// Data transformations for charts +const preparePriceChartData = ( + data: [number, number][] | undefined +): ChartDataPoint[] => { + if (!data || !Array.isArray(data)) return []; + + return data.map((item) => ({ + date: new Date(item[0]).toLocaleDateString(), + value: item[1], + })); +}; + +// Tab Panel component +const TabPanel = (props: TabPanelProps) => { + const { children, value, index, ...other } = props; + + return ( + + ); +}; + +const AssetInfoModal = ({ + open, + onClose, + asset, + tradingVolume7d, + userBalance = 0, + pools = [], + loading = false, +}: AssetInfoModalProps) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const [tabValue, setTabValue] = useState(0); -const AssetInfoModal = ({ open, onClose, asset }: AssetInfoModalProps) => { + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setTabValue(newValue); + }; + + const handleCopyToClipboard = useCallback((text: string) => { + navigator.clipboard.writeText(text); + }, []); + + // Chart data + const volumeData = prepareVolumeChartData( + (tradingVolume7d as TradingVolume[]) || [] + ); + + const priceData = preparePriceChartData(asset.price7d || []); + + // Modal styles using the app's style constants const style = { position: "absolute" as "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)", - width: "min(512px, 90%)", - maxWidth: "100vw", - background: "#1F1F1F", - borderRadius: "16px", - boxShadow: "0px 4px 24px rgba(0, 0, 0, 0.6)", + width: { xs: "95%", sm: "90%", md: "85%", lg: "75%" }, + maxWidth: "1200px", + height: { xs: "95vh", md: "85vh" }, // Set explicit height instead of maxHeight + background: colors.neutral[900], + borderRadius: borderRadius.lg, + boxShadow: shadows.card, display: "flex", flexDirection: "column" as "column", - padding: "24px", overflow: "hidden", }; - const handleCopyToClipboard = useCallback((text) => { - navigator.clipboard.writeText(text); - }, []); - - return ( - - - {/* Background Icon */} - {asset.tomlInfo.image && ( - - )} - - {/* Close Button */} - - - + // Chart height constant to ensure consistency + const chartHeight = 300; + const chartCardHeight = 420; - {/* Header Section */} + // Custom tooltip component for better styling + const CustomTooltip = ({ active, payload, label }: any) => { + if (active && payload && payload.length) { + return ( - Asset Information + {label} - - - {/* Asset Details */} - - - + - Quick facts about - - {asset.tomlInfo.image && ( - - - - )} + /> - {asset.tomlInfo.code}: + {payload[0].dataKey === "value" ? "Price: " : "Volume: "} + + {payload[0].value.toFixed(6)} + - - - - - Domain - - + ); + } + return null; + }; + + return ( + + + + + {loading ? ( + + + + Loading asset data... + + + ) : ( + <> + {/* Header */} + - {asset.domain} - - - {asset.supply && ( - - - Total Supply - - - {(Number(asset.supply) / 10 ** 7).toFixed()} {asset.tomlInfo.image && ( )} - {asset.tomlInfo.code} - - - )} - - - First Transaction - - - {new Date(asset.created * 1000).toLocaleString("en-US", { - timeZone: "UTC", - dateStyle: "medium", - timeStyle: "medium", - }) + " UTC"} - - - - - Trustlines - - - {asset.trustlines[2]} funded / {asset.trustlines[0]} total - - - - - Total Payments - - + + {asset.tomlInfo.code} + {asset.domain && ( + + + ({asset.domain}) + + + )} + + + {userBalance > 0 && ( + + Your Balance: {formatNumber(userBalance)}{" "} + {asset.tomlInfo.code} + + )} + + + + + + + + + {/* Content Area */} + - {new Intl.NumberFormat("en-US").format(asset.payments)} - - - -
-
-
+ {/* Tabs */} + + + + + + + + + {/* Loading State or Tab Content */} + {loading ? ( + + + + Loading asset data... + + + ) : ( + + {/* Tab content with scrollable area */} + {/* Overview Tab */} + + + + {/* Key metrics */} + + + + + Key Metrics + + + Powered by stellar.expert + + + + + + + + + Total Supply + + + {asset.supply + ? formatNumber( + Number(asset.supply) / 10 ** 7 + ) + : "N/A"} + + + + + + + + Total Payments + + + {formatNumber(asset.payments)} + + + + + + + + Trustlines + + + {asset.trustlines[2]} funded /{" "} + {asset.trustlines[0]} total + + + + + + + + First Transaction + + + {new Date( + asset.created * 1000 + ).toLocaleDateString()} + + + + + + + + Issuer + + + + {shortenAddress(asset.tomlInfo.issuer)} + + + handleCopyToClipboard( + asset.tomlInfo.issuer + ) + } + sx={{ color: colors.neutral[400] }} + > + + + + + + + + + + + + {/* Asset Rating section has been removed */} + + + + {/* Charts Tab */} + + + {/* Price Chart */} + + + + Price (USDC) - 7 Days + + + {volumeData.length > 0 ? ( + + + + + + + + + + + + } + cursor={{ + stroke: colors.neutral[600], + strokeDasharray: "3 3", + }} + /> + + + + + ) : ( + + + No price data available + + + )} + + + + {/* Volume Chart */} + + + + Volume - 7 Days + + + + + Total Volume (7d): + {formatNumber( + // @ts-ignore + calculateTotalVolume(tradingVolume7d) + )}{" "} + USDC + + + + {volumeData.length > 0 ? ( + + + + + + + + + + + + } + cursor={{ + fill: colors.neutral[800], + opacity: 0.3, + }} + /> + + + + + ) : ( + + + No volume data available + + + )} + + + + + + {/* Pools Tab */} + + {pools.length > 0 ? ( + + {pools.map((pool) => ( + { + window.location.href = `/pools/${pool.poolAddress}`; + }} + onAddLiquidityClick={() => {}} + /> + ))} + + ) : ( + + + + + No liquidity pools available for this asset + + + + )} + + + )} +
+ + )} + + +
); }; diff --git a/packages/ui/src/Dashboard/CryptoCTA/CryptoCTA.stories.tsx b/packages/ui/src/Dashboard/CryptoCTA/CryptoCTA.stories.tsx index 49a42d7c..3dd5d58b 100644 --- a/packages/ui/src/Dashboard/CryptoCTA/CryptoCTA.stories.tsx +++ b/packages/ui/src/Dashboard/CryptoCTA/CryptoCTA.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import CryptoCTA from "./CryptoCTA"; import { Grid } from "@mui/material"; +import React from "react"; // Default metadata of the story https://storybook.js.org/docs/react/api/csf#default-export const meta: Meta = { diff --git a/packages/ui/src/Dashboard/CryptoCTA/CryptoCTA.tsx b/packages/ui/src/Dashboard/CryptoCTA/CryptoCTA.tsx index ab44d678..4740f6c6 100644 --- a/packages/ui/src/Dashboard/CryptoCTA/CryptoCTA.tsx +++ b/packages/ui/src/Dashboard/CryptoCTA/CryptoCTA.tsx @@ -7,12 +7,11 @@ const CryptoCTA = ({ onClick }: CryptoCTAProps) => { return ( { Need More Crypto? - + You can easily deposit now! + + + + {/* Type Filter */} + + + Type + + + + + {/* Platform Filter */} + + + Platform + + + + + {/* Instant Unbond Toggle */} + + + } + label={ + + Instant unbond only + + } + sx={{ ml: 0, mr: 0 }} + /> + + + + + + ); +}; diff --git a/packages/ui/src/Earn/Modals/BondModal.stories.tsx b/packages/ui/src/Earn/Modals/BondModal.stories.tsx new file mode 100644 index 00000000..7fe4f82c --- /dev/null +++ b/packages/ui/src/Earn/Modals/BondModal.stories.tsx @@ -0,0 +1,185 @@ +import React, { useState } from "react"; +import { Meta, StoryObj } from "@storybook/react"; +import { BondModal } from "./BondModal"; +import { Button } from "../../Button/Button"; // Import Button to trigger modal +import { StrategyMetadata, ContractType } from "@phoenix-protocol/strategies"; +import { Token } from "@phoenix-protocol/types"; + +const meta: Meta = { + title: "Earn/Modals/BondModal", + component: BondModal, + parameters: { + layout: "centered", // Center the button that opens the modal + }, + argTypes: { + open: { control: "boolean" }, + onClose: { action: "closed" }, + onConfirm: { action: "confirmed" }, + strategy: { control: "object" }, + }, +}; + +export default meta; + +type Story = StoryObj; + +const mockStrategy: StrategyMetadata = { + id: "mock-strategy-1", + providerId: "mock-provider", + name: "Mock Yield Strategy", + description: "A mock strategy for testing", + assets: [ + { + name: "XLM", + icon: "/cryptoIcons/xlm.svg", + amount: 0, + category: "native", + usdValue: 0.11, + }, + ], + tvl: 100000, + apr: 0.05, + rewardToken: { + name: "PHO", + icon: "/cryptoIcons/pho.svg", + amount: 0, + category: "phoenix", + usdValue: 0.02, + }, + unbondTime: 0, + category: "yield", + available: true, + contractAddress: "MOCK_CONTRACT_ADDRESS", + contractType: "stake" as ContractType, + userStake: 0, + userRewards: 0, + hasJoined: false, +}; + +const mockLPStrategy: StrategyMetadata = { + ...mockStrategy, + id: "mock-lp-strategy", + name: "XLM-USDC Liquidity Pool", + description: "Provide liquidity to earn PHO rewards", + assets: [ + { + name: "XLM", + icon: "/cryptoIcons/xlm.svg", + amount: 0, + category: "native", + usdValue: 0.11, + }, + { + name: "USDC", + icon: "/cryptoIcons/usdc.svg", + amount: 0, + category: "token", + usdValue: 1.0, + }, + ], + category: "farming", + contractType: "pair" as ContractType, +}; + +const mockTripleAssetStrategy: StrategyMetadata = { + ...mockStrategy, + id: "mock-triple-asset-strategy", + name: "Triple Asset Pool", + description: "Provide liquidity to a stable pool", + assets: [ + { + name: "USDC", + icon: "/cryptoIcons/usdc.svg", + amount: 0, + category: "token", + usdValue: 1.0, + }, + { + name: "USDT", + icon: "/cryptoIcons/usdt.svg", + amount: 0, + category: "token", + usdValue: 0.999, + }, + { + name: "DAI", + icon: "/cryptoIcons/dai.svg", + amount: 0, + category: "token", + usdValue: 0.998, + }, + ], + category: "farming", + contractType: "pair" as ContractType, // Using "pair" for triple asset pool too +}; + +const ModalWrapper = (args) => { + const [open, setOpen] = useState(args.open || false); + + const handleOpen = () => setOpen(true); + const handleClose = () => { + setOpen(false); + args.onClose(); // Call the action + }; + const handleConfirm = (tokenAmounts) => { + args.onConfirm(tokenAmounts); // Call the action + console.log("Token amounts:", tokenAmounts); + setOpen(false); // Close modal on confirm + }; + + return ( + <> + + + + ); +}; + +export const Default: Story = { + render: ModalWrapper, + args: { + // Args for the wrapper, not the modal directly + strategy: mockStrategy, + // open: false, // Initial state is closed + }, +}; + +export const PreOpened: Story = { + render: ModalWrapper, + args: { + strategy: mockStrategy, + open: true, // Start with the modal open + }, +}; + +export const LiquidityPair: Story = { + render: ModalWrapper, + args: { + strategy: mockLPStrategy, + }, +}; + +export const PreOpenedLiquidityPair: Story = { + render: ModalWrapper, + args: { + strategy: mockLPStrategy, + open: true, + }, +}; + +export const TripleAssetPool: Story = { + render: ModalWrapper, + args: { + strategy: mockTripleAssetStrategy, + }, +}; diff --git a/packages/ui/src/Earn/Modals/BondModal.tsx b/packages/ui/src/Earn/Modals/BondModal.tsx new file mode 100644 index 00000000..70551c13 --- /dev/null +++ b/packages/ui/src/Earn/Modals/BondModal.tsx @@ -0,0 +1,322 @@ +import React, { useState, useEffect, useMemo, useCallback } from "react"; +import { Box, Modal, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { motion } from "framer-motion"; +import { Button } from "../../Button/Button"; +import { TokenBox } from "../../Swap"; // Import TokenBox from Swap components +import { + colors, + typography, + spacing, + borderRadius, +} from "../../Theme/styleConstants"; +import { StrategyMetadata } from "@phoenix-protocol/strategies"; +import CloseIcon from "@mui/icons-material/Close"; +import { Token } from "@phoenix-protocol/types"; + +interface BondModalProps { + open: boolean; + onClose: () => void; + strategy: StrategyMetadata | null; + onConfirm: (amounts: { token: Token; amount: number }[]) => void; +} + +export const BondModal = ({ + open, + onClose, + strategy, + onConfirm, +}: BondModalProps) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + + // Track amounts for all assets with an object keyed by token index + const [amounts, setAmounts] = useState<{ [key: number]: string }>({}); + const [error, setError] = useState(""); + + const isPairStrategy = + strategy?.contractType === "pair" && strategy.assets.length > 1; + + // Reset state when modal opens or strategy changes + useEffect(() => { + if (open && strategy) { + const initialAmounts = {}; + strategy.assets.forEach((_, index) => { + initialAmounts[index] = ""; + }); + setAmounts(initialAmounts); + setError(""); + } + }, [open, strategy]); + + // Calculate token ratios for pair strategies + const tokenRatios = useMemo(() => { + if (!isPairStrategy || !strategy?.assets || strategy.assets.length <= 1) { + return {}; + } + + // Calculate ratios relative to the first token + const ratios = {}; + const baseValue = strategy.assets[0].usdValue || 1; + + strategy.assets.forEach((asset, index) => { + if (index === 0) { + ratios[index] = 1; // Base asset has ratio 1 + } else { + // Calculate how many of this token equals 1 of the base token + ratios[index] = baseValue / (asset.usdValue || 1); + } + }); + + return ratios; + }, [isPairStrategy, strategy?.assets]); + + // Handle amount changes and maintain ratios for pair strategies + const handleAmountChange = useCallback( + (changedIndex: number, newValue: string) => { + const updatedAmounts = { ...amounts }; + updatedAmounts[changedIndex] = newValue; + + if ( + isPairStrategy && + strategy?.assets && + Object.keys(tokenRatios).length > 0 + ) { + const numValue = parseFloat(newValue); + + if (!isNaN(numValue) && numValue > 0) { + strategy.assets.forEach((_asset, tokenIdx) => { + if (tokenIdx !== changedIndex) { + // Calculate N_j = N_k * (ratios_j / ratios_k) + // N_k is numValue (amount of asset k, at changedIndex) + // N_j is amount of asset j (at tokenIdx) + // ratios_k is tokenRatios[changedIndex] + // ratios_j is tokenRatios[tokenIdx] + if ( + tokenRatios[changedIndex] != null && + tokenRatios[changedIndex] !== 0 && + tokenRatios[tokenIdx] != null + ) { + const relativeRatio = + tokenRatios[tokenIdx] / tokenRatios[changedIndex]; + updatedAmounts[tokenIdx] = (numValue * relativeRatio).toFixed( + 6 + ); + } else { + // Fallback if ratios are not available or division by zero would occur + updatedAmounts[tokenIdx] = ""; + } + } + }); + } else if (newValue === "" || numValue === 0) { + // If the input is cleared or set to 0, clear other fields in a pair strategy + strategy.assets.forEach((_asset, tokenIdx) => { + if (tokenIdx !== changedIndex) { + updatedAmounts[tokenIdx] = ""; + } + }); + } + } + + setAmounts(updatedAmounts); + setError(""); // Clear error when user types + }, + [amounts, isPairStrategy, strategy?.assets, tokenRatios] + ); + + const handleConfirm = () => { + // Validate all required amounts are provided + const tokenAmounts = + strategy?.assets.map((token, index) => { + const amount = amounts[index]; + const numericAmount = parseFloat(amount); + return { + token, + amount: isNaN(numericAmount) ? 0 : numericAmount, + }; + }) || []; + + // Check if any required amount is missing or invalid + const invalidAmount = tokenAmounts.some( + ({ amount }, index) => amount <= 0 && (isPairStrategy || index === 0) // For single asset, only first token required + ); + + if (invalidAmount) { + setError("Please enter valid positive amounts for all required tokens."); + return; + } + + // Call the onConfirm handler with the token amounts + onConfirm(tokenAmounts); + + // Reset state + const resetAmounts = {}; + strategy?.assets.forEach((_, index) => { + resetAmounts[index] = ""; + }); + setAmounts(resetAmounts); + setError(""); + + onClose(); + }; + + const handleClose = () => { + // Reset state + const resetAmounts = {}; + strategy?.assets.forEach((_, index) => { + resetAmounts[index] = ""; + }); + setAmounts(resetAmounts); + setError(""); + onClose(); + }; + + // Check if the form is valid for submission + const isFormValid = useMemo(() => { + if (!strategy) return false; + + // For pair strategies, all assets must have valid amounts + if (isPairStrategy) { + return strategy.assets.every((_, index) => { + const amount = parseFloat(amounts[index] || "0"); + return !isNaN(amount) && amount > 0; + }); + } + // For single asset strategies, only the first asset needs a valid amount + else { + const primaryAmount = parseFloat(amounts[0] || "0"); + return !isNaN(primaryAmount) && primaryAmount > 0; + } + }, [strategy, amounts, isPairStrategy]); + + if (!strategy) return null; + + return ( + + + + + + {isPairStrategy ? "Provide Liquidity" : "Bond to"} {strategy.name} + + + + + + + + + + {isPairStrategy + ? `Enter the amount of tokens you want to provide as liquidity.` + : `Enter the amount of ${ + strategy.assets[0]?.name || "tokens" + } you want to bond.`} + + + {/* Token inputs for all assets */} + {strategy.assets.map((asset, index) => ( + 0 ? spacing.md : 0 }}> + handleAmountChange(index, value)} + token={asset} + hideDropdownButton + /> + + ))} + + {isPairStrategy && ( + + The ratio of tokens will be automatically maintained. + + )} + + {error && ( + + {error} + + )} + + + + + + ); +}; diff --git a/packages/ui/src/Earn/Modals/ClaimAllModal.stories.tsx b/packages/ui/src/Earn/Modals/ClaimAllModal.stories.tsx new file mode 100644 index 00000000..0b48f4fa --- /dev/null +++ b/packages/ui/src/Earn/Modals/ClaimAllModal.stories.tsx @@ -0,0 +1,205 @@ +import React, { useState, useEffect } from "react"; +import { Meta, StoryObj } from "@storybook/react"; +import { action } from "@storybook/addon-actions"; +import { Box, Button } from "@mui/material"; +import { + Strategy, + StrategyMetadata, + Token, + ContractType, + IndividualStake, +} from "@phoenix-protocol/strategies"; +import { AssembledTransaction } from "@stellar/stellar-sdk/lib/contract"; +import { ClaimAllModal } from "./ClaimAllModal"; // Assuming ClaimAllModalProps is part of this or defined below + +// Replicating ClaimAllModalProps for clarity if not exported directly +interface ClaimAllModalProps { + open: boolean; + onClose: () => void; + claimableStrategies: { + strategy: Strategy; + metadata: StrategyMetadata; + rewards: number; + }[]; + onClaimStrategy: ( + strategy: Strategy, + metadata: StrategyMetadata + ) => Promise; +} + +const mockToken = (name: string, symbol: string): Token => ({ + name, + icon: `/cryptoIcons/${symbol.toLowerCase()}.svg`, + amount: 0, + usdValue: Math.random() * 100, + category: "mock", + address: `C${"X".repeat(55)}`, +}); + +const mockStrategyMetadata = ( + id: string, + name: string, + rewards: number +): StrategyMetadata => ({ + id, + providerId: `mock-provider-${id}`, + name, + description: `This is a mock description for ${name}. It involves staking and earning rewards.`, + assets: [mockToken("Token A", "TKA"), mockToken("Token B", "TKB")], + tvl: Math.random() * 1000000, + apr: Math.random() * 0.25, + rewardToken: mockToken("Reward Token", "RWD"), + unbondTime: 86400 * 7, // 7 days + category: "Liquidity Provision", + available: true, + contractAddress: `CA${id.toUpperCase()}${"X".repeat(50 - id.length)}`, + contractType: "stake" as ContractType, + userStake: rewards > 0 ? Math.random() * 10000 : 0, + userRewards: rewards, + hasJoined: rewards > 0, + userIndividualStakes: [], // Set to empty array to avoid BigInt serialization in Storybook +}); + +const mockStrategy = (metadata: StrategyMetadata): Strategy => ({ + getMetadata: async () => metadata, + getUserStake: async () => metadata.userStake || 0, + hasUserJoined: async () => metadata.hasJoined || false, + getUserRewards: async () => metadata.userRewards || 0, + bond: async () => ({} as unknown as AssembledTransaction), + unbond: async () => ({} as unknown as AssembledTransaction), + claim: async () => ({} as unknown as AssembledTransaction), +}); + +const mockOnClaimStrategy = ( + _strategy: Strategy, + metadata: StrategyMetadata +): Promise => { + action("onClaimStrategy")(metadata.name); + return new Promise((resolve, reject) => { + setTimeout(() => { + if (metadata.name.includes("Fails")) { + action("onClaimStrategy - Error")(metadata.name); + reject(new Error(`Simulated claim failure for ${metadata.name}`)); + } else { + action("onClaimStrategy - Success")(metadata.name); + resolve(); + } + }, 1500 + Math.random() * 1000); + }); +}; + +const meta: Meta = { + title: "Earn/Modals/ClaimAllModal", + component: ClaimAllModal, + argTypes: { + open: { control: "boolean" }, + onClose: { action: "onClose" }, + claimableStrategies: { control: "object" }, + onClaimStrategy: { action: "onClaimStrategy" }, + }, + parameters: { + layout: "centered", + }, + decorators: [ + (Story, context) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [isOpen, setIsOpen] = useState(context.args.open); + + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + setIsOpen(context.args.open); + }, [context.args.open]); + + const handleOpen = () => setIsOpen(true); + const handleClose = () => { + setIsOpen(false); + action("onClose")(); // Call the Storybook action + }; + + return ( + + {!isOpen && ( + + )} + + + ); + }, + ], +}; + +export default meta; + +type Story = StoryObj; + +const strategyMetas = [ + mockStrategyMetadata("s1", "Alpha Liquidity Pool", 125.5), + mockStrategyMetadata("s2", "Beta Staking (Sometimes Fails)", 75.2), + mockStrategyMetadata("s3", "Gamma Yield Farm", 200.0), + mockStrategyMetadata("s4", "Delta Vault", 50.75), +]; + +const mockClaimableStrategiesData = strategyMetas + .filter((meta) => meta.userRewards && meta.userRewards > 0) + .map((meta) => ({ + strategy: mockStrategy(meta), + metadata: meta, + rewards: meta.userRewards || 0, + })); + +export const Default: Story = { + args: { + open: false, + claimableStrategies: mockClaimableStrategiesData, + onClaimStrategy: mockOnClaimStrategy, + onClose: action("onClose"), + }, +}; + +export const WithFailingStrategy: Story = { + args: { + ...Default.args, + claimableStrategies: [ + { + strategy: mockStrategy( + mockStrategyMetadata("s1-fail", "Stable Yield", 100) + ), + metadata: mockStrategyMetadata("s1-fail", "Stable Yield", 100), + rewards: 100, + }, + { + strategy: mockStrategy( + mockStrategyMetadata("s2-fail", "Risky Bet (Fails)", 50) + ), + metadata: mockStrategyMetadata("s2-fail", "Risky Bet (Fails)", 50), + rewards: 50, + }, + { + strategy: mockStrategy( + mockStrategyMetadata("s3-fail", "Safe Bet", 120) + ), + metadata: mockStrategyMetadata("s3-fail", "Safe Bet", 120), + rewards: 120, + }, + ], + }, +}; + +export const Empty: Story = { + args: { + ...Default.args, + claimableStrategies: [], + }, +}; + +export const SingleItem: Story = { + args: { + ...Default.args, + claimableStrategies: + mockClaimableStrategiesData.length > 0 + ? [mockClaimableStrategiesData[0]] + : [], + }, +}; diff --git a/packages/ui/src/Earn/Modals/ClaimAllModal.tsx b/packages/ui/src/Earn/Modals/ClaimAllModal.tsx new file mode 100644 index 00000000..eb728f90 --- /dev/null +++ b/packages/ui/src/Earn/Modals/ClaimAllModal.tsx @@ -0,0 +1,425 @@ +import React, { useEffect, useState, useCallback, useRef } from "react"; +import { + Modal, + Box, + Typography, + CircularProgress, + List, + ListItem, + ListItemIcon, + ListItemText, + useTheme, + useMediaQuery, + IconButton, + Paper, + LinearProgress, + Alert, +} from "@mui/material"; +import { + CheckCircleOutline, + ErrorOutline, + HourglassEmpty, + Close as CloseIcon, + PlayCircleOutline as ClaimIcon, +} from "@mui/icons-material"; +import { Strategy, StrategyMetadata } from "@phoenix-protocol/strategies"; +import { formatCurrencyStatic } from "@phoenix-protocol/utils"; +import { Button } from "../../Button/Button"; + +type ClaimStatus = "pending" | "claiming" | "success" | "error"; + +interface ClaimableStrategyItem { + strategy: Strategy; + metadata: StrategyMetadata; + rewards: number; + status: ClaimStatus; + error?: string; +} + +interface ClaimAllModalProps { + open: boolean; + onClose: () => void; + claimableStrategies: { + strategy: Strategy; + metadata: StrategyMetadata; + rewards: number; + }[]; + onClaimStrategy: ( + strategy: Strategy, + metadata: StrategyMetadata + ) => Promise; // Function to execute the claim transaction +} + +export const ClaimAllModal = ({ + open, + onClose, + claimableStrategies, + onClaimStrategy, +}: ClaimAllModalProps) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + + const [items, setItems] = useState([]); + const itemsRef = useRef([]); + + const [currentIndex, setCurrentIndex] = useState(-1); + const [isClaiming, setIsClaiming] = useState(false); + const [isComplete, setIsComplete] = useState(false); + + useEffect(() => { + if (open) { + // Only re-initialize fully if not in an active claim cycle AND not in a "completed" state. + // A "completed" state should persist until the modal is closed or a new claim cycle is started. + if (!isClaiming && !isComplete) { + const initialItems = claimableStrategies.map((s) => ({ + ...s, + status: "pending" as ClaimStatus, + error: undefined, + })); + setItems(initialItems); + itemsRef.current = initialItems; + setCurrentIndex(-1); + // isClaiming is false, isComplete is false, so no need to set them here. + // If opening for the first time or after a full close and reopen, isComplete would be false. + } + } else { + // When modal is closed, ensure all states are reset for the next opening. + // This helps if the modal is closed mid-operation or in a completed state. + setIsClaiming(false); + setIsComplete(false); + setCurrentIndex(-1); + // setItems([]); // Optionally clear items, or let the open condition repopulate. + // itemsRef.current = []; + } + }, [open, claimableStrategies, isClaiming, isComplete]); // Added isComplete to the dependency array + + const handleCloseModal = () => { + setIsClaiming(false); // Stop any ongoing claiming process + onClose(); + }; + + useEffect(() => { + if (!isClaiming || currentIndex < 0) { + return; + } + + const currentCycleItems = itemsRef.current; // Use the ref for the logical list + + if (currentIndex >= currentCycleItems.length) { + if (currentCycleItems.length > 0 || currentIndex > 0) { + setIsComplete(true); + } + setIsClaiming(false); // All items processed or list was empty + return; + } + + const itemToProcess = currentCycleItems[currentIndex]; + let mounted = true; + + if (itemToProcess && itemToProcess.status === "pending") { + const doClaim = async (indexBeingProcessed: number) => { + // Update UI state to 'claiming' for the item at indexBeingProcessed + setItems((prevUIItems) => + prevUIItems.map((uiItem, idx) => { + // Ensure we are matching by a stable ID if prevUIItems could be from a different source than itemsRef.current + // However, with the revised first useEffect, prevUIItems should be consistent with itemsRef for statuses. + if (uiItem.metadata.id === itemToProcess.metadata.id) { + // Match by ID + return { ...uiItem, status: "claiming" }; + } + return uiItem; + }) + ); + // Update ref status to 'claiming' + if (itemsRef.current[indexBeingProcessed]) { + // Check bounds + itemsRef.current[indexBeingProcessed].status = "claiming"; + } + + let finalStatus: ClaimStatus; + let finalError: string | undefined; + + try { + await onClaimStrategy(itemToProcess.strategy, itemToProcess.metadata); + finalStatus = "success"; + } catch (error: any) { + finalStatus = "error"; + finalError = error?.message || "An unknown error occurred"; + } + + if (mounted) { + // Update UI state with final status for the item at indexBeingProcessed + setItems((prevUIItems) => + prevUIItems.map((uiItem, idx) => { + if (uiItem.metadata.id === itemToProcess.metadata.id) { + // Match by ID + return { ...uiItem, status: finalStatus, error: finalError }; + } + return uiItem; + }) + ); + // Update ref with final status + if (itemsRef.current[indexBeingProcessed]) { + // Check bounds + itemsRef.current[indexBeingProcessed].status = finalStatus; + itemsRef.current[indexBeingProcessed].error = finalError; + } + + if (isClaiming) { + setCurrentIndex((prev) => prev + 1); + } + } + }; + doClaim(currentIndex); + } else if ( + itemToProcess && + itemToProcess.status !== "claiming" && + isClaiming + ) { + // If item is not 'pending' (e.g. already success/error from a previous attempt in this cycle, or bad state) + // and not currently 'claiming', skip to next. + if (mounted) { + setCurrentIndex((prev) => prev + 1); + } + } + + return () => { + mounted = false; + }; + }, [isClaiming, currentIndex, onClaimStrategy]); + + const startClaiming = useCallback(() => { + // This re-initializes itemsRef.current and items state for a *new* claiming cycle + const freshCycleItems = claimableStrategies.map((s) => ({ + ...s, + status: "pending" as ClaimStatus, + error: undefined, + })); + itemsRef.current = freshCycleItems; + setItems(freshCycleItems); // Also update UI state to reflect the fresh list + + if (itemsRef.current.length === 0) { + setIsComplete(true); + setIsClaiming(false); // Ensure isClaiming is false if nothing to claim + return; + } + // Do not proceed if already claiming from a previous click. + // This check might be redundant if button is disabled, but good for safety. + if (isClaiming) return; + + setIsComplete(false); + setCurrentIndex(0); // Start from the first item + setIsClaiming(true); // This will trigger the processing useEffect + }, [claimableStrategies, isClaiming]); // isClaiming is a dependency for the safety check + + const totalRewards = items.reduce((sum, item) => sum + item.rewards, 0); + const claimedCount = items.filter((item) => item.status === "success").length; + const errorCount = items.filter((item) => item.status === "error").length; + const progress = + items.length > 0 ? ((claimedCount + errorCount) / items.length) * 100 : 0; + + const renderStatusIcon = (status: ClaimStatus) => { + switch (status) { + case "pending": + return ; + case "claiming": + return ( + + ); + case "success": + return ( + + ); + case "error": + return ; + default: + return null; + } + }; + + return ( + + + + + {isComplete + ? "Claiming Complete" + : isClaiming + ? currentIndex >= 0 && currentIndex < itemsRef.current.length // Use itemsRef for name consistency during cycle + ? `Claiming: ${ + itemsRef.current[currentIndex]?.metadata.name || // Get name from ref + `Item ${currentIndex + 1}` + }` + : "Claiming Rewards..." + : "Claim All Rewards"} + + + + + + + + + Total Rewards to Claim:{" "} + + {formatCurrencyStatic.format(totalRewards)} + + + {(isClaiming || isComplete) && ( + + + + {claimedCount + errorCount} / {items.length} processed + + + )} + + + + + {items.map((item, index) => ( + + + {renderStatusIcon(item.status)} + + + {item.metadata.name} + + } + secondary={ + + Rewards: {formatCurrencyStatic.format(item.rewards)} + {item.status === "error" && + item.error && + ` - Error: ${item.error}`} + + } + /> + + ))} + + + + {!isClaiming && !isComplete && items.length > 0 && ( + + )} + {isClaiming && !isComplete && ( + + Please confirm each transaction in your wallet. + + )} + {isComplete && ( + + + Successfully claimed: {claimedCount} strategy(s). + + {errorCount > 0 && ( + + Failed to claim: {errorCount} strategy(s). + + )} + + + )} + {items.length === 0 && !isClaiming && !isComplete && ( + + No claimable rewards available. + + )} + + + ); +}; diff --git a/packages/ui/src/Earn/Modals/UnbondModal.stories.tsx b/packages/ui/src/Earn/Modals/UnbondModal.stories.tsx new file mode 100644 index 00000000..e088d96a --- /dev/null +++ b/packages/ui/src/Earn/Modals/UnbondModal.stories.tsx @@ -0,0 +1,215 @@ +import React, { useState } from "react"; +import { Meta, StoryObj } from "@storybook/react"; +import { UnbondModal } from "./UnbondModal"; +import { Button } from "../../Button/Button"; +import { + StrategyMetadata, + ContractType, + IndividualStake, +} from "@phoenix-protocol/strategies"; + +const meta: Meta = { + title: "Earn/Modals/UnbondModal", + component: UnbondModal, + parameters: { + layout: "centered", + }, + argTypes: { + open: { control: "boolean" }, + onClose: { action: "closed" }, + onConfirm: { action: "confirmed" }, + strategy: { control: "object" }, + maxAmount: { control: "number" }, + }, +}; + +export default meta; + +type Story = StoryObj; + +const mockStrategyAssets = [ + { + name: "XLM", + icon: "/cryptoIcons/xlm.svg", + amount: 0, + category: "native", + usdValue: 0.11, + }, +]; + +const mockLPAssets = [ + { + name: "XLM", + icon: "/cryptoIcons/xlm.svg", + amount: 0, + category: "native", + usdValue: 0.11, + }, + { + name: "USDC", + icon: "/cryptoIcons/usdc.svg", + amount: 0, + category: "token", + usdValue: 1.0, + }, +]; + +const mockStrategy: StrategyMetadata = { + id: "mock-strategy-1", + providerId: "mock-provider", + name: "Mock Yield Strategy", + description: "A mock strategy for testing", + assets: mockStrategyAssets, + tvl: 100000, + apr: 0.05, + rewardToken: { + name: "PHO", + icon: "/cryptoIcons/pho.svg", + amount: 0, + category: "phoenix", + usdValue: 0.02, + }, + unbondTime: 0, // Instant + category: "yield", + available: true, + contractAddress: "MOCK_CONTRACT_ADDRESS", + contractType: "stake" as ContractType, + userStake: 1000, // Example stake + userRewards: 10, + hasJoined: true, +}; + +const mockStrategyWithLock: StrategyMetadata = { + ...mockStrategy, + id: "mock-strategy-lock", + name: "Mock Locked Strategy", + unbondTime: 604800, // 7 days + userStake: 500, +}; + +const mockLPStrategyWithIndividualStakes: StrategyMetadata = { + id: "mock-lp-strategy-individual", + providerId: "mock-provider", + name: "Mock LP Strategy", + description: "LP strategy with individual stakes", + assets: mockLPAssets, + tvl: 250000, + apr: 0.12, + rewardToken: { + name: "PHO", + icon: "/cryptoIcons/pho.svg", + amount: 0, + category: "phoenix", + usdValue: 0.02, + }, + unbondTime: 0, // Instant for LP example + category: "liquidity", + available: true, + contractAddress: "MOCK_LP_CONTRACT_ADDRESS", + contractType: "pair" as ContractType, + userStake: 1500, // Total USD value of all stakes + userRewards: 25, + hasJoined: true, + userIndividualStakes: [ + { + lpAmount: BigInt("1000000000"), + timestamp: BigInt(Math.floor(new Date("2023-01-15").getTime() / 1000)), + displayAmount: "100.00 LP", + displayDate: "2023-01-15", + }, + { + lpAmount: BigInt("500000000"), + timestamp: BigInt(Math.floor(new Date("2023-02-20").getTime() / 1000)), + displayAmount: "50.00 LP", + displayDate: "2023-02-20", + }, + { + lpAmount: BigInt("2000000000"), + timestamp: BigInt(Math.floor(new Date("2023-03-10").getTime() / 1000)), + displayAmount: "200.00 LP", + displayDate: "2023-03-10", + }, + ], +}; + +const ModalWrapper = (args) => { + const [open, setOpen] = useState(args.open || false); + + const handleOpen = () => setOpen(true); + const handleClose = () => { + setOpen(false); + args.onClose(); + }; + const handleConfirm = ( + params: number | { lpAmount: bigint; timestamp: bigint } + ) => { + args.onConfirm(params); + // Do not close modal here, let the parent (EarnPage) handle it after transaction. + // setOpen(false); + }; + + return ( + <> + + + + ); +}; + +export const Default: Story = { + render: ModalWrapper, + args: { + strategy: mockStrategy, + maxAmount: 1000, + }, +}; + +export const WithLockPeriod: Story = { + render: ModalWrapper, + args: { + strategy: mockStrategyWithLock, + maxAmount: 500, + }, +}; + +export const PreOpened: Story = { + render: ModalWrapper, + args: { + strategy: mockStrategy, + maxAmount: 1000, + open: true, + }, +}; + +export const ZeroMaxAmount: Story = { + render: ModalWrapper, + args: { + strategy: mockStrategy, + maxAmount: 0, + }, +}; + +export const LPStrategyWithIndividualStakes: Story = { + render: ModalWrapper, + args: { + strategy: mockLPStrategyWithIndividualStakes, + maxAmount: mockLPStrategyWithIndividualStakes.userStake, // Total stake value + // open: false, // Default to closed + }, +}; + +export const PreOpenedLPStrategyWithIndividualStakes: Story = { + render: ModalWrapper, + args: { + strategy: mockLPStrategyWithIndividualStakes, + maxAmount: mockLPStrategyWithIndividualStakes.userStake, + open: true, + }, +}; diff --git a/packages/ui/src/Earn/Modals/UnbondModal.tsx b/packages/ui/src/Earn/Modals/UnbondModal.tsx new file mode 100644 index 00000000..31237432 --- /dev/null +++ b/packages/ui/src/Earn/Modals/UnbondModal.tsx @@ -0,0 +1,344 @@ +import React, { useState, useEffect } from "react"; +import { + Box, + Modal, + Typography, + TextField, + useMediaQuery, + useTheme, + Link, + List, + ListItem, + ListItemText, + IconButton, + Divider, +} from "@mui/material"; +import { motion } from "framer-motion"; +import { Button } from "../../Button/Button"; +import { + colors, + typography, + spacing, + borderRadius, +} from "../../Theme/styleConstants"; +import { + StrategyMetadata, + IndividualStake, +} from "@phoenix-protocol/strategies"; +import CloseIcon from "@mui/icons-material/Close"; +import { formatCurrencyStatic } from "@phoenix-protocol/utils"; // Assuming you have this utility + +interface UnbondModalProps { + open: boolean; + onClose: () => void; + strategy: StrategyMetadata | null; + maxAmount: number; // User's current total stake in this strategy (USD value) + onConfirm: (params: number | { lpAmount: bigint; timestamp: bigint }) => void; +} + +export const UnbondModal = ({ + open, + onClose, + strategy, + maxAmount, + onConfirm, +}: UnbondModalProps) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const [amount, setAmount] = useState(""); + const [error, setError] = useState(""); + + useEffect(() => { + // Reset state when modal opens or strategy changes + if (open) { + setAmount(""); + setError(""); + } + }, [open, strategy]); + + const handleAmountChange = (event: React.ChangeEvent) => { + const value = event.target.value; + if (/^\d*\.?\d*$/.test(value)) { + setAmount(value); + const numericValue = parseFloat(value); + if (numericValue > maxAmount) { + setError(`Maximum unbond amount is ${maxAmount}`); + } else { + setError(""); + } + } + }; + + const handleSetMax = () => { + setAmount(maxAmount.toString()); + setError(""); // Clear error when setting max + }; + + const handleConfirmAmount = () => { + const numericAmount = parseFloat(amount); + if (isNaN(numericAmount) || numericAmount <= 0) { + setError("Please enter a valid positive amount."); + return; + } + if (numericAmount > maxAmount) { + setError(`Maximum unbond amount is ${maxAmount}`); + return; + } + onConfirm(numericAmount); + onClose(); // Close after confirmation + }; + + const handleConfirmSpecificStake = (stake: IndividualStake) => { + onConfirm({ lpAmount: stake.lpAmount, timestamp: stake.timestamp }); + // Optionally close modal immediately or wait for parent component to do so after transaction + // onClose(); + }; + + const handleClose = () => { + setAmount(""); + setError(""); + onClose(); + }; + + if (!strategy) return null; + + const showIndividualStakes = + strategy.contractType === "pair" && + strategy.userIndividualStakes && + strategy.userIndividualStakes.length > 0; + + return ( + + + + + + Unbond from {strategy.name} + + + + + + + + + {showIndividualStakes ? ( + <> + + Select a stake to unbond its full amount. + + {strategy.unbondTime > 0 && ( + + Note: Unbonding period is approximately{" "} + {Math.ceil(strategy.unbondTime / 86400)} days. + + )} + + {strategy.userIndividualStakes!.map((stake, index) => ( + + handleConfirmSpecificStake(stake)} + > + Unbond + + } + sx={{ paddingRight: "100px" }} // Ensure space for button + > + + + {index < strategy.userIndividualStakes!.length - 1 && ( + + )} + + ))} + + + ) : ( + <> + + Enter the amount you want to unbond. + + + Available to unbond:{" "} + + {formatCurrencyStatic.format(maxAmount)}{" "} + {strategy.assets.map((a) => a.name).join(" / ")} + + + + {strategy.unbondTime > 0 && ( + + Note: Unbonding period is approximately{" "} + {Math.ceil(strategy.unbondTime / 86400)} days. + + )} + + + + + + )} + + + + ); +}; diff --git a/packages/ui/src/Earn/StrategiesTable/StrategiesTable.stories.tsx b/packages/ui/src/Earn/StrategiesTable/StrategiesTable.stories.tsx new file mode 100644 index 00000000..c9a1a859 --- /dev/null +++ b/packages/ui/src/Earn/StrategiesTable/StrategiesTable.stories.tsx @@ -0,0 +1,239 @@ +import React from "react"; +import { Meta, StoryObj } from "@storybook/react"; +import { StrategiesTable } from "./StrategiesTable"; +import { Grid } from "@mui/material"; +import { action } from "@storybook/addon-actions"; +import { StrategyMetadata, ContractType } from "@phoenix-protocol/strategies"; + +const meta: Meta = { + title: "Earn/StrategiesTable", + decorators: [ + (Story) => ( + + + + + + ), + ], + argTypes: { + title: { control: "text" }, + strategies: { control: "object" }, + showFilters: { control: "boolean" }, + isLoading: { control: "boolean" }, + onViewDetails: { action: "viewDetails" }, + onBondClick: { action: "bondClicked" }, + onUnbondClick: { action: "unbondClicked" }, + emptyStateMessage: { control: "text" }, + }, +}; + +export default meta; + +type Story = StoryObj; + +const Template = (args: any) => ; + +export const Default: Story = { + render: Template, + args: { + title: "Discover Strategies", + strategies: [ + { + id: "stellar-yield-strategy", + assets: [ + { + name: "XLM", + icon: "/cryptoIcons/xlm.svg", + amount: 0, + category: "native", + usdValue: 0.11, + }, + ], + name: "Stellar Yield", + description: "Stake XLM to earn PHO rewards", + tvl: 123456, + apr: 0.05, + rewardToken: { + name: "PHO", + icon: "/cryptoIcons/pho.svg", + amount: 0, + category: "phoenix", + usdValue: 0.02, + }, + unbondTime: 0, + isMobile: false, + link: "/earn/stellar-yield-strategy", + category: "yield", + providerId: "stellar", + available: true, + contractAddress: "MOCK_CONTRACT_ADDRESS", + contractType: "stake" as ContractType, + }, + { + id: "phoenix-boost-strategy", + isMobile: false, + assets: [ + { + name: "XLM", + icon: "/cryptoIcons/xlm.svg", + amount: 0, + category: "native", + usdValue: 0.11, + }, + ], + name: "Phoenix Boost", + description: "Stake XLM to earn PHO rewards at a boosted rate", + tvl: 789012, + apr: 0.1, + rewardToken: { + name: "PHO", + icon: "/cryptoIcons/pho.svg", + amount: 0, + category: "phoenix", + usdValue: 0.02, + }, + unbondTime: 604800, + link: "/earn/phoenix-boost-strategy", + category: "staking", + providerId: "phoenix", + available: true, + contractAddress: "MOCK_CONTRACT_ADDRESS", + contractType: "stake" as ContractType, + }, + ], + showFilters: true, + isLoading: false, + onViewDetails: action("viewDetails"), + onBondClick: action("bondClicked"), + onUnbondClick: action("unbondClicked"), + }, +}; + +export const WithUserData: Story = { + render: Template, + args: { + title: "Your Strategies", + strategies: [ + { + id: "stellar-yield-strategy", + assets: [ + { + name: "XLM", + icon: "/cryptoIcons/xlm.svg", + amount: 0, + category: "native", + usdValue: 0.11, + }, + ], + name: "Stellar Yield", + description: "Stake XLM to earn PHO rewards", + tvl: 123456, + apr: 0.05, + rewardToken: { + name: "PHO", + address: "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA", + icon: "/cryptoIcons/pho.svg", + amount: 0, + category: "phoenix", + usdValue: 0.02, + }, + unbondTime: 0, + isMobile: false, + link: "/earn/stellar-yield-strategy", + category: "yield", + providerId: "stellar", + hasJoined: true, + userStake: 1000, + userRewards: 25.5, + available: true, + contractAddress: "MOCK_CONTRACT_ADDRESS", + contractType: "stake" as ContractType, + }, + { + id: "phoenix-boost-strategy", + isMobile: false, + assets: [ + { + name: "XLM", + address: "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA", + icon: "/cryptoIcons/xlm.svg", + amount: 0, + category: "native", + usdValue: 0.11, + }, + ], + name: "Phoenix Boost", + description: "Stake XLM to earn PHO rewards at a boosted rate", + tvl: 789012, + apr: 0.1, + rewardToken: { + name: "PHO", + address: "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA", + icon: "/cryptoIcons/pho.svg", + amount: 0, + category: "phoenix", + usdValue: 0.02, + }, + unbondTime: 604800, + link: "/earn/phoenix-boost-strategy", + category: "staking", + providerId: "phoenix", + hasJoined: true, + userStake: 5000, + userRewards: 75.25, + available: true, + contractAddress: "MOCK_CONTRACT_ADDRESS", + contractType: "stake" as ContractType, + }, + ], + showFilters: false, + isLoading: false, + onViewDetails: action("viewDetails"), + onBondClick: action("bondClicked"), + onUnbondClick: action("unbondClicked"), + }, +}; + +export const Loading: Story = { + render: Template, + args: { + title: "Discover Strategies", + strategies: [], + showFilters: true, + isLoading: true, + onViewDetails: action("viewDetails"), + onBondClick: action("bondClicked"), + onUnbondClick: action("unbondClicked"), + }, +}; + +export const Empty: Story = { + render: Template, + args: { + title: "Your Strategies", + strategies: [], + showFilters: false, + isLoading: false, + onViewDetails: action("viewDetails"), + onBondClick: action("bondClicked"), + onUnbondClick: action("unbondClicked"), + emptyStateMessage: + "You haven't joined any strategies yet. Discover strategies to start earning!", + }, +}; + +export const EmptyDiscover: Story = { + render: Template, + args: { + title: "Discover Strategies", + strategies: [], + showFilters: true, + isLoading: false, + onViewDetails: action("viewDetails"), + onBondClick: action("bondClicked"), + onUnbondClick: action("unbondClicked"), + emptyStateMessage: + "No new strategies to discover at the moment. Check back later!", + }, +}; diff --git a/packages/ui/src/Earn/StrategiesTable/StrategiesTable.tsx b/packages/ui/src/Earn/StrategiesTable/StrategiesTable.tsx new file mode 100644 index 00000000..453c6db4 --- /dev/null +++ b/packages/ui/src/Earn/StrategiesTable/StrategiesTable.tsx @@ -0,0 +1,382 @@ +import React, { useState, useMemo } from "react"; +import { + Box, + Grid, + Typography, + useMediaQuery, + useTheme, + CircularProgress, + TableSortLabel, +} from "@mui/material"; +import { motion } from "framer-motion"; +import { FilterBar } from "../FilterBar/FilterBar"; +import StrategyEntry from "./StrategyEntry"; +import { StrategyMetadata } from "@phoenix-protocol/strategies"; +import { + colors, + typography, + spacing, + borderRadius, +} from "../../Theme/styleConstants"; + +export interface StrategiesTableProps { + title: string; + strategies: StrategyMetadata[]; + showFilters?: boolean; + isLoading?: boolean; + onViewDetails?: (id: string) => void; + onBondClick: (strategy: StrategyMetadata) => void; + onUnbondClick: (strategy: StrategyMetadata) => void; + emptyStateMessage?: string; +} + +type SortField = "tvl" | "apr" | null; +type SortDirection = "asc" | "desc"; + +export const StrategiesTable = ({ + title, + strategies, + showFilters = true, + isLoading = false, + onViewDetails = () => {}, + onBondClick, + onUnbondClick, + emptyStateMessage = "No strategies match your criteria.", // Default message +}: StrategiesTableProps) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + + // Filter states + const [assetsFilter, setAssetsFilter] = useState< + "Your assets" | "All Assets" + >("All Assets"); + const [typeFilter, setTypeFilter] = useState("All"); + const [platformFilter, setPlatformFilter] = useState("All"); + const [instantUnbondOnly, setInstantUnbondOnly] = useState(false); + + // Sort states + const [sortField, setSortField] = useState(null); + const [sortDirection, setSortDirection] = useState("desc"); + + const handleSort = (field: SortField) => { + if (sortField === field) { + // Toggle direction + setSortDirection(sortDirection === "asc" ? "desc" : "asc"); + } else { + // New field, default to descending + setSortField(field); + setSortDirection("desc"); + } + }; + + const types = ["LP Staking", "Single Asset Staking", "Farming"]; + const platforms = ["Phoenix", "External"]; + + // Apply filters and sorting + const filteredStrategies = useMemo(() => { + if (isLoading || !strategies.length) return []; + + let filtered = [...strategies]; + + // Asset filter (would need to be implemented based on user assets) + if (assetsFilter === "Your assets") { + // Check for userAssetMatch with correct null/undefined handling + filtered = filtered.filter((s) => s.hasJoined !== false); + } + + // Type filter + if (typeFilter !== "All") { + filtered = filtered.filter( + (s) => s.category === typeFilter.toLowerCase() + ); + } + + // Platform filter + if (platformFilter !== "All") { + filtered = filtered.filter( + (s) => s.providerId === platformFilter.toLowerCase() + ); + } + + // Instant unbond only + if (instantUnbondOnly) { + filtered = filtered.filter((s) => s.unbondTime === 0); + } + + // Apply sorting + if (sortField) { + filtered.sort((a, b) => { + const valueA = a[sortField] || 0; + const valueB = b[sortField] || 0; + + if (sortDirection === "asc") { + return valueA - valueB; + } else { + return valueB - valueA; + } + }); + } + + return filtered; + }, [ + strategies, + assetsFilter, + typeFilter, + platformFilter, + instantUnbondOnly, + sortField, + sortDirection, + isLoading, + ]); + + // Helper function for table headers + const renderSortableHeader = ( + field: SortField, + label: string, + width: number + ) => ( + + handleSort(field)} + sx={{ + color: colors.neutral[300], + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeights.bold, + textTransform: "uppercase", + "& .MuiTableSortLabel-icon": { + color: `${colors.neutral[300]} !important`, + }, + }} + > + {label} + + + ); + + return ( + + + {showFilters && ( + setInstantUnbondOnly(value)} + types={types} + platforms={platforms} + /> + )} + + {/* Table header - only show on desktop */} + {!isMobile && ( + + + {/* Assets Column - 2/12 width */} + + + Assets + + + + {/* Strategy Name Column - 2/12 width */} + + + Strategy + + + + {/* TVL Column - sortable, 1/12 width */} + {renderSortableHeader("tvl", "TVL", 1)} + + {/* APR Column - sortable, 1/12 width */} + {renderSortableHeader("apr", "APR", 1)} + + {/* Reward Token Column - 1/12 or 2/12 width */} + + + Reward + + + + {/* Your Stake Column - only show if any strategy has user joined */} + {hasAnyUserJoined(strategies) && ( + + + Your Stake + + + )} + + {/* Claimable Rewards Column - only show if any strategy has user joined */} + {hasAnyUserJoined(strategies) && ( + + + Claimable + + + )} + + {/* Unbond Time Column - only show if no strategy has user joined */} + {!hasAnyUserJoined(strategies) && ( + + + Unbond Time + + + )} + + {/* Action Column - Adjusted to md={2} */} + + + Action + + + + + )} + + {/* Mobile title for strategies - only show on mobile */} + {isMobile && ( + + {filteredStrategies.length} strategies found + + )} + + {/* Loading state */} + {isLoading ? ( + + + + ) : filteredStrategies.length > 0 ? ( + + {filteredStrategies.map((strategy, index) => ( + + + + ))} + + ) : ( + + + {emptyStateMessage} + + + )} + + + ); +}; + +// Helper function to check if any strategy has the user joined +const hasAnyUserJoined = (strategies: StrategyMetadata[]): boolean => { + return strategies.some((strategy) => strategy.hasJoined); +}; diff --git a/packages/ui/src/Earn/StrategiesTable/StrategyEntry.tsx b/packages/ui/src/Earn/StrategiesTable/StrategyEntry.tsx new file mode 100644 index 00000000..ed24d2d2 --- /dev/null +++ b/packages/ui/src/Earn/StrategiesTable/StrategyEntry.tsx @@ -0,0 +1,560 @@ +import React from "react"; +import { Box, Grid, Typography, Tooltip, useTheme, Chip } from "@mui/material"; +import { motion } from "framer-motion"; +import { formatCurrencyStatic } from "@phoenix-protocol/utils"; +import { + colors, + typography, + spacing, + borderRadius, +} from "../../Theme/styleConstants"; +import { Token } from "@phoenix-protocol/types"; +import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; +import { Button } from "../../Button/Button"; // Import Button +import { StrategyMetadata } from "@phoenix-protocol/strategies"; // Import StrategyMetadata + +export interface StrategyEntryProps { + // Use StrategyMetadata directly for clarity + strategy: StrategyMetadata; + isMobile: boolean; + onViewDetails: (id: string) => void; // Keep for potential future use or different click areas + onBondClick: (strategy: StrategyMetadata) => void; // Callback for Bond action + onUnbondClick: (strategy: StrategyMetadata) => void; // Callback for Unbond action +} + +const BoxStyle = { + p: spacing.md, + borderRadius: borderRadius.md, + background: colors.neutral[900], + border: `1px solid ${colors.neutral[700]}`, + position: "relative", + overflow: "hidden", + boxShadow: "0px 4px 12px rgba(0, 0, 0, 0.15)", + marginTop: spacing.sm, + transition: "all 0.3s ease", + "&:hover": { + boxShadow: "0px 8px 16px rgba(0, 0, 0, 0.25)", + borderColor: colors.neutral[600], + // cursor: "pointer", // Remove default cursor pointer for the whole box + }, +}; + +const StrategyEntry = ({ + strategy, // Use the full metadata object + isMobile, + onViewDetails, // Keep if needed elsewhere + onBondClick, + onUnbondClick, +}: StrategyEntryProps) => { + const theme = useTheme(); + const { + id, + name, + description, + tvl, + apr, + rewardToken, + unbondTime, + assets, + userStake = 0, + userRewards = 0, + hasJoined = false, + } = strategy; // Destructure metadata + + // Format unbond time for display + const formatUnbondTime = (time: number) => { + if (time === 0) return "Instant"; + const days = Math.floor(time / 86400); + const hours = Math.floor((time % 86400) / 3600); + + if (days > 0) return `${days} days`; + if (hours > 0) return `${hours} hours`; + return "< 1 hour"; + }; + + return ( + // Removed motion.div and onClick from the outer Box to avoid conflicting clicks + + {/* Background asset graphics */} + {assets.map((asset, index) => ( + + ))} + + {/* Mobile Layout */} + {isMobile ? ( + + {/* Header with assets and name */} + + + {assets.map((asset, idx) => ( + + ))} + + + {name} + + + + {/* Description if available */} + {description && ( + + {description} + + )} + + {/* Strategy details */} + + + + TVL + + + {formatCurrencyStatic.format(tvl)} + + + + + + APR + + + up to {(apr * 100).toFixed(1)}% + + + + {/* User stake - only show if user has joined */} + {hasJoined && ( + <> + + + {formatCurrencyStatic.format(userStake)} + + + + + + + 0 + ? colors.success[300] + : colors.neutral[300], + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeights.medium, + }} + > + {userRewards.toFixed(2)} {rewardToken.name} + + + + + )} + + {!hasJoined && ( + <> + + + Reward + + + + + {rewardToken.name} + + + + + + + Unbond Time + + + {formatUnbondTime(unbondTime)} + + + + )} + + + {/* Action Button - Updated for mobile */} + {hasJoined ? ( + + + + + ) : ( + + )} + + ) : ( + /* Desktop Layout */ + + {/* Assets Column */} + + + {assets.map((asset, idx) => ( + + + + {asset.name} + + + ))} + + + + {/* Strategy Name */} + + + + {name} + + + + + {/* TVL */} + + + {formatCurrencyStatic.format(tvl)} + + + + {/* APR */} + + + {(apr * 100).toFixed(1)}% + + + + {/* Reward Token */} + + + + {rewardToken.name} + + + + {/* Your Stake - only show if joined */} + {hasJoined && ( + + + Your Stake + + + {formatCurrencyStatic.format(userStake)} + + + )} + + {/* Claimable rewards - only show if joined */} + {hasJoined && ( + + + Claimable + + + + 0 + ? colors.success[300] + : colors.neutral[300], + }} + > + {userRewards.toFixed(2)} {rewardToken.name} + + + + )} + + {/* Unbond Time - only show if not joined */} + {!hasJoined && ( + + + {formatUnbondTime(unbondTime)} + + + )} + + {/* Action Button Column - Updated for desktop */} + + {hasJoined ? ( + + + + + ) : ( + + + + )} + + + )} + + ); +}; + +export default StrategyEntry; diff --git a/packages/ui/src/Earn/YieldSummary/YieldSummary.stories.tsx b/packages/ui/src/Earn/YieldSummary/YieldSummary.stories.tsx new file mode 100644 index 00000000..d13a6b03 --- /dev/null +++ b/packages/ui/src/Earn/YieldSummary/YieldSummary.stories.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { Meta, StoryObj } from "@storybook/react"; +import { YieldSummary } from "./YieldSummary"; +import { Grid } from "@mui/material"; + +const meta: Meta = { + title: "Earn/YieldSummary", + decorators: [ + (Story) => ( + + + + + + ), + ], + argTypes: { + totalValue: { control: "number" }, + claimableRewards: { control: "number" }, + onClaimAll: { action: "clicked" }, + }, +}; + +export default meta; + +type Story = StoryObj; + +const Template = (args: any) => ; + +export const Default: Story = { + render: Template, + args: { + totalValue: 12345.67, + claimableRewards: 123.45, + }, +}; diff --git a/packages/ui/src/Earn/YieldSummary/YieldSummary.tsx b/packages/ui/src/Earn/YieldSummary/YieldSummary.tsx new file mode 100644 index 00000000..b3f7a81c --- /dev/null +++ b/packages/ui/src/Earn/YieldSummary/YieldSummary.tsx @@ -0,0 +1,158 @@ +import React from "react"; +import { Box, Grid, Typography } from "@mui/material"; +import { motion } from "framer-motion"; +import { Button } from "../../Button/Button"; +import { formatCurrencyStatic } from "@phoenix-protocol/utils"; +import { + colors, + typography, + spacing, + borderRadius, +} from "../../Theme/styleConstants"; + +export interface YieldSummaryProps { + totalValue: number; + claimableRewards: number; + onClaimAll: () => void; +} + +export const YieldSummary = ({ + totalValue, + claimableRewards, + onClaimAll, +}: YieldSummaryProps) => { + return ( + + + {/* Background graphic */} + + + + + + + Total Value Staked + + + {formatCurrencyStatic.format(totalValue)} + + + Stake your assets to earn passive income through various yield + strategies + + + + + + + + Claimable Rewards + + + {formatCurrencyStatic.format(claimableRewards)} + + + + + + + + ); +}; diff --git a/packages/ui/src/Earn/index.ts b/packages/ui/src/Earn/index.ts new file mode 100644 index 00000000..984b5ba9 --- /dev/null +++ b/packages/ui/src/Earn/index.ts @@ -0,0 +1,8 @@ +export * from "./FilterBar/FilterBar"; +export * from "./StrategiesTable/StrategiesTable"; +export * from "./YieldSummary/YieldSummary"; +export * from "./EarnPage"; // Assuming EarnPage component is here or moved here +// Export Modals +export * from "./Modals/BondModal"; +export * from "./Modals/UnbondModal"; +export * from "./Modals/ClaimAllModal"; diff --git a/packages/ui/src/HelpCenter/ArticleCard/ArticleCard.stories.tsx b/packages/ui/src/HelpCenter/ArticleCard/ArticleCard.stories.tsx index 87636aff..457152cf 100644 --- a/packages/ui/src/HelpCenter/ArticleCard/ArticleCard.stories.tsx +++ b/packages/ui/src/HelpCenter/ArticleCard/ArticleCard.stories.tsx @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { ArticleCard } from "./ArticleCard"; import example from "./example.json"; import { Grid, Container } from "@mui/material"; +import React from "react"; // Default metadata of the story https://storybook.js.org/docs/react/api/csf#default-export const meta: Meta = { diff --git a/packages/ui/src/HelpCenter/ArticleCard/ArticleCard.tsx b/packages/ui/src/HelpCenter/ArticleCard/ArticleCard.tsx index c461dd91..b5eeeda7 100644 --- a/packages/ui/src/HelpCenter/ArticleCard/ArticleCard.tsx +++ b/packages/ui/src/HelpCenter/ArticleCard/ArticleCard.tsx @@ -20,11 +20,10 @@ const ArticleCard = ({ article }: { article: HelpCenterArticle }) => { { left: "1rem", top: "1rem", borderRadius: "1rem", - border: "1px solid var(--Primary-P3, #E2571C)", - background: "#3D3D3D", + border: "1px solid var(--primary-500, #F97316)", + background: "rgba(226, 73, 26, 0.10)", }} /> { left: "1rem", top: "1rem", borderRadius: "1rem", - border: "1px solid var(--Primary-P3, #E2571C)", + border: "1px solid var(--primary-500, #F97316)", background: "rgba(226, 73, 26, 0.10)", }} > { width="440px" image={`https://phoenix-helpcenter.pockethost.io/api/files/${collectionId}/${id}/${thumbnail}`} alt="Article image" - sx={{ bgcolor: "#2E2E2E" }} + sx={{ bgcolor: "var(--neutral-800, #262626)" }} /> { variant="body2" sx={{ overflow: "hidden", - color: "var(--Secondary-S2-2, #BDBEBE)", + color: "var(--neutral-300, #D4D4D4)", textOverflow: "ellipsis", whiteSpace: "nowrap", fontFamily: "Ubuntu", diff --git a/packages/ui/src/HelpCenter/CategoryCard/CategoryCard.stories.tsx b/packages/ui/src/HelpCenter/CategoryCard/CategoryCard.stories.tsx index 006571b1..5040441e 100644 --- a/packages/ui/src/HelpCenter/CategoryCard/CategoryCard.stories.tsx +++ b/packages/ui/src/HelpCenter/CategoryCard/CategoryCard.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import CategoryCard from "./CategoryCard"; import { Container, Grid } from "@mui/material"; +import React from "react"; // Default metadata of the story https://storybook.js.org/docs/react/api/csf#default-export const meta: Meta = { diff --git a/packages/ui/src/HelpCenter/CategoryCard/CategoryCard.tsx b/packages/ui/src/HelpCenter/CategoryCard/CategoryCard.tsx index e4852481..db059059 100644 --- a/packages/ui/src/HelpCenter/CategoryCard/CategoryCard.tsx +++ b/packages/ui/src/HelpCenter/CategoryCard/CategoryCard.tsx @@ -16,20 +16,20 @@ const CategoryCard = ({ category }: CategoryCardProps) => { { src={thumbnail} sx={{ display: "flex", - width: "5rem", - height: "5rem", + width: "4rem", + height: "4rem", justifyContent: "center", alignItems: "center", flexShrink: 0, @@ -52,16 +52,21 @@ const CategoryCard = ({ category }: CategoryCardProps) => { /> {name} {description} diff --git a/packages/ui/src/Modal/Modal.tsx b/packages/ui/src/Modal/Modal.tsx index cb45850f..fd51df02 100644 --- a/packages/ui/src/Modal/Modal.tsx +++ b/packages/ui/src/Modal/Modal.tsx @@ -6,10 +6,11 @@ import { Modal as MuiModal, Typography, } from "@mui/material"; -import Colors from "../Theme/colors"; import { Button } from "../Button/Button"; import { ModalProps } from "@phoenix-protocol/types"; import SwapAnimation from "./SwapAnimation"; +import { motion } from "framer-motion"; +import { colors, typography, spacing, borderRadius, shadows } from "../Theme/styleConstants"; const Modal = ({ type, @@ -30,42 +31,46 @@ const Modal = ({ transform: "translate(-50%, -50%)", width: 317, maxWidth: "calc(100vw - 16px)", - background: "linear-gradient(180deg, #292B2C 0%, #1F2123 100%)", - borderRadius: "16px", + background: colors.gradients.card, + borderRadius: borderRadius.lg, display: "flex", flexDirection: "column" as "column", - padding: "16px", + padding: spacing.md, + boxShadow: shadows.card, }; const tokenHeaderStyle = { color: "rgba(255, 255, 255, 0.70)", - fontSize: "12px", - fontWeight: 400, + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeights.regular, + fontFamily: typography.fontFamily, lineHeight: "140%", - marginBottom: "8px", + marginBottom: spacing.xs, }; const tokenIconStyle = { width: "24px", height: "24px", - marginRight: "8px", + marginRight: spacing.xs, }; const tokenAmountStyle = { - color: "#FFF", - fontSize: "14px", - fontWeight: 700, + color: colors.neutral[50], + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeights.bold, + fontFamily: typography.fontFamily, lineHeight: "140%", }; const getAsset = () => { - if (type == "SUCCESS") { + if (type === "SUCCESS") { return "/check.svg"; - } else if (type == "WARNING") { + } else if (type === "WARNING") { return "/warning.svg"; - } else if (type == "ERROR") { + } else if (type === "ERROR") { return "/cross.svg"; } + return ""; }; const phoIconStyle = { @@ -77,196 +82,203 @@ const Modal = ({ setOpen(false)} - aria-labelledby="connectwallet-modal" - aria-describedby="connect your wallet to the app" + aria-labelledby="modal-title" + aria-describedby="modal-description" > - - - + + + setOpen(false)} - component="img" sx={{ - display: "inline-flex", - justifyContent: "center", - alignItems: "center", - w: "16px", - h: "16px", - backgroundColor: Colors.inputsHover, - borderRadius: "8px", - cursor: "pointer", + display: "flex", + justifyContent: "flex-end", }} - src="/x.svg" - /> - - - {type == "LOADING" || type == "LOADING_SWAP" ? ( + > setOpen(false)} + component="img" sx={{ - h: "98px", - width: type == "LOADING_SWAP" ? "60%" : "98px", - marginBottom: "12px", - display: "flex", + display: "inline-flex", justifyContent: "center", alignItems: "center", + width: "16px", + height: "16px", + backgroundColor: colors.neutral[800], + borderRadius: borderRadius.sm, + cursor: "pointer", }} - > - {type == "LOADING_SWAP" ? ( - // @ts-ignore - - ) : ( - - )} - - ) : ( - - )} - + - {title} - - - {!!tokens && type !== "LOADING_SWAP" && ( - + {type === "LOADING_SWAP" ? ( + // @ts-ignore + + ) : ( + + )} + + ) : ( + + )} + + {title} + + + {!!tokens && type !== "LOADING_SWAP" && ( - - 1 ? 6 : 12}> - - {tokenTitles[0]} - - - - - {tokenAmounts[0]} - - - - {tokens.length > 1 && ( - + + + 1 ? 6 : 12}> - {tokenTitles[1]} + {tokenTitles[0]} - {tokenAmounts[1]} + {tokenAmounts[0]} - )} - + {tokens.length > 1 && ( + + + {tokenTitles[1]} + + + + + {tokenAmounts[1]} + + + + )} + + - - )} + )} - {description && ( - - - {description} - - - )} - {error && ( - - + + {description} + + + )} + {error && ( + + + {error} + + + )} + {onButtonClick && type !== "LOADING_SWAP" && error && ( + - + + {" "} + {/* Adjusted color */} Available {balance.toFixed(2)} @@ -224,10 +243,9 @@ const ClaimRewards = ({ return ( - + + {" "} + {/* Adjusted color */} Total rewards {rewards.length > 0 ? ( @@ -250,16 +276,32 @@ const ClaimRewards = ({ - + + {" "} + {/* Adjusted color and weight */} {reward.amount} {reward.name} )) ) : ( - + + {" "} + {/* Adjusted color and weight */} No rewards @@ -274,7 +316,12 @@ const ClaimRewards = ({ position: "absolute", bottom: "1rem", width: "calc(100% - 2rem)", - background: rewards.length > 0 ? "#E2621B" : "#6F6F6F", + background: + rewards.length > 0 ? "#F97316" : "var(--neutral-700, #404040)", // Adjusted color + "&:hover": { + background: + rewards.length > 0 ? "#F97316" : "var(--neutral-700, #404040)", // Adjusted color + }, }} onClick={onClaim} disabled={rewards.length === 0} @@ -315,11 +362,21 @@ const LiquidityMining = ({ fontSize: "1.125rem", fontWeight: 700, marginBottom: 0, + color: "var(--neutral-50, #FAFAFA)", // Adjusted color }} > Liquidity Mining - + + {" "} + {/* Adjusted color */} Bond liquidity to earn liquidity reward and swap fees diff --git a/packages/ui/src/PoolSingle/Overview/Overview.stories.tsx b/packages/ui/src/PoolSingle/Overview/Overview.stories.tsx index c45aec3d..30163c73 100644 --- a/packages/ui/src/PoolSingle/Overview/Overview.stories.tsx +++ b/packages/ui/src/PoolSingle/Overview/Overview.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import Overview from "./Overview"; +import React from "react"; // Default metadata of the story https://storybook.js.org/docs/react/api/csf#default-export const meta: Meta = { diff --git a/packages/ui/src/PoolSingle/PoolLiquidity/PoolLiquidity.stories.tsx b/packages/ui/src/PoolSingle/PoolLiquidity/PoolLiquidity.stories.tsx index 57b4af6d..dbb9dc25 100644 --- a/packages/ui/src/PoolSingle/PoolLiquidity/PoolLiquidity.stories.tsx +++ b/packages/ui/src/PoolSingle/PoolLiquidity/PoolLiquidity.stories.tsx @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import PoolLiquidity from "./PoolLiquidity"; import { Grid } from "@mui/material"; import { testTokens } from "../../Dashboard/WalletBalanceTable/WalletBalanceTable.stories"; +import React from "react"; // Default metadata of the story https://storybook.js.org/docs/react/api/csf#default-export const meta: Meta = { diff --git a/packages/ui/src/PoolSingle/PoolLiquidity/PoolLiquidity.tsx b/packages/ui/src/PoolSingle/PoolLiquidity/PoolLiquidity.tsx index 92d6f9eb..bd6491e0 100644 --- a/packages/ui/src/PoolSingle/PoolLiquidity/PoolLiquidity.tsx +++ b/packages/ui/src/PoolSingle/PoolLiquidity/PoolLiquidity.tsx @@ -13,6 +13,7 @@ import { TokenBox } from "../../Swap"; import { Button } from "../../Button/Button"; import { LabTabProps, PoolLiquidityProps } from "@phoenix-protocol/types"; import { motion } from "framer-motion"; +import { borderRadius, colors, typography } from "../../Theme/styleConstants"; /** * GlowingChart Component @@ -32,8 +33,8 @@ const GlowingChart = ({ data }: { data: number[][] }) => ( - - + + ( v[1]} - stroke="#E2491A" + stroke="#F97316" strokeWidth={2} filter="url(#neonGlow)" fill="url(#chartFill)" @@ -95,16 +96,17 @@ const LabTabs = ({ const buttonStyles = { flex: 1, maxWidth: "200px", - color: "#FFF", - fontSize: "0.875rem", - fontWeight: 700, + color: colors.neutral[50], + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeights.medium, textTransform: "none", - borderRadius: "12px", + borderRadius: borderRadius.md, transition: "all 0.3s", - backgroundImage: - "linear-gradient(95.06deg, rgb(226, 73, 26) 0%, rgb(226, 27, 27) 16.92%, rgb(226, 73, 26) 42.31%, rgb(226, 170, 27) 99.08%)", + background: colors.neutral[900], + border: `1px solid ${colors.neutral[700]}`, "&:hover": { transform: "scale(1.05)", + background: colors.neutral[800], }, }; @@ -116,11 +118,12 @@ const LabTabs = ({ onClick={() => setValue("1")} sx={{ ...buttonStyles, - backgroundImage: - value === "1" - ? "linear-gradient(95.06deg, rgb(226, 73, 26) 0%, rgb(226, 27, 27) 16.92%, rgb(226, 73, 26) 42.31%, rgb(226, 170, 27) 99.08%)" - : "none", - filter: value === "1" ? "brightness(1.2)" : "brightness(1)", + background: + value === "1" ? "#F97316" : "var(--neutral-900, #171717)", + "&:hover": { + background: + value === "1" ? "#F97316" : "var(--neutral-800, #262626)", + }, }} > Add Liquidity @@ -129,11 +132,12 @@ const LabTabs = ({ onClick={() => setValue("2")} sx={{ ...buttonStyles, - backgroundImage: - value === "2" - ? "linear-gradient(95.06deg, rgb(226, 73, 26) 0%, rgb(226, 27, 27) 16.92%, rgb(226, 73, 26) 42.31%, rgb(226, 170, 27) 99.08%)" - : "none", - filter: value === "2" ? "brightness(1.2)" : "brightness(1)", + background: + value === "2" ? "#F97316" : "var(--neutral-900, #171717)", + "&:hover": { + background: + value === "2" ? "#F97316" : "var(--neutral-800, #262626)", + }, }} > Remove Liquidity @@ -204,63 +208,98 @@ const PoolLiquidity = ({ return ( - + Pool Liquidity - + - + {tokenA.name} - + {liquidityA} {tokenB.name} - + {liquidityB} Ratio - + 1:{(liquidityB / liquidityA).toFixed(2)} diff --git a/packages/ui/src/PoolSingle/PoolStats/PoolStats.stories.tsx b/packages/ui/src/PoolSingle/PoolStats/PoolStats.stories.tsx index a02d895f..1f5d33b0 100644 --- a/packages/ui/src/PoolSingle/PoolStats/PoolStats.stories.tsx +++ b/packages/ui/src/PoolSingle/PoolStats/PoolStats.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import PoolStats from "./PoolStats"; import { Grid } from "@mui/material"; +import React from "react"; // Default metadata of the story https://storybook.js.org/docs/react/api/csf#default-export const meta: Meta = { diff --git a/packages/ui/src/PoolSingle/PoolStats/PoolStats.tsx b/packages/ui/src/PoolSingle/PoolStats/PoolStats.tsx index c6115332..9d76a650 100644 --- a/packages/ui/src/PoolSingle/PoolStats/PoolStats.tsx +++ b/packages/ui/src/PoolSingle/PoolStats/PoolStats.tsx @@ -22,36 +22,39 @@ const PoolStatsBox = ({ title, value }: PoolStatsBoxProps) => { {title.toUpperCase()} {value} diff --git a/packages/ui/src/PoolSingle/StakingList/StakingList.stories.tsx b/packages/ui/src/PoolSingle/StakingList/StakingList.stories.tsx index ce2c8658..9d4c4402 100644 --- a/packages/ui/src/PoolSingle/StakingList/StakingList.stories.tsx +++ b/packages/ui/src/PoolSingle/StakingList/StakingList.stories.tsx @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import StakingList from "./StakingList"; import { Grid } from "@mui/material"; import { StakingListEntry as Entry } from "@phoenix-protocol/types"; +import React from "react"; // Default metadata of the story https://storybook.js.org/docs/react/api/csf#default-export const meta: Meta = { diff --git a/packages/ui/src/PoolSingle/StakingList/StakingList.tsx b/packages/ui/src/PoolSingle/StakingList/StakingList.tsx index 1039e319..8a4255de 100644 --- a/packages/ui/src/PoolSingle/StakingList/StakingList.tsx +++ b/packages/ui/src/PoolSingle/StakingList/StakingList.tsx @@ -19,8 +19,8 @@ const StakingEntry = ({ entry }: { entry: Entry }) => { sx={{ p: 2, borderRadius: "12px", - background: - "linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.03) 100%)", + background: "var(--neutral-900, #171717)", // Adjusted background + border: "1px solid var(--neutral-700, #404040)", // Adjusted border mb: 2, }} > @@ -30,26 +30,42 @@ const StakingEntry = ({ entry }: { entry: Entry }) => { component="img" src="/cryptoIcons/poolIcon.png" alt="Pool Icon" - sx={{ width: "32px", height: "32px", mr: 2 }} + sx={{ width: "32px", height: "32px", mr: 2, opacity: 0.7 }} // Adjusted opacity /> {entry.title} - + + {" "} + {/* Adjusted color and size */} {entry.apr} APR - + + {" "} + {/* Adjusted color and size */} Locked: {entry.lockedPeriod} - + + {" "} + {/* Adjusted color and size */} {entry.amount.tokenAmount} (${entry.amount.tokenValueInUsd}) @@ -57,24 +73,26 @@ const StakingEntry = ({ entry }: { entry: Entry }) => { - + {/* Adjusted size */} Unstake @@ -97,7 +115,7 @@ const StakingList = ({ entries }: { entries: Entry[] }) => { {/* Header */} { ) : ( @@ -66,7 +73,10 @@ const FilterButton = React.memo( } ); -const PoolItem = React.memo( +/** + * Pool Item Component + */ +export const PoolItem = React.memo( ({ pool, filter, @@ -86,35 +96,40 @@ const PoolItem = React.memo( md={4} lg={3} xl={3} - onClick={() => onShowDetailsClick(pool)} component={motion.div} initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.4, ease: "easeInOut" }} - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} + whileHover={{ scale: 1.03 }} + whileTap={{ scale: 0.98 }} + onClick={() => onShowDetailsClick(pool)} > - {/* Logos in the background */} + {/* Background Logos */} {`${pool.tokens[0].name} - ${pool.tokens[1].name}`} @@ -178,20 +193,19 @@ const PoolItem = React.memo( {/* Pool Stats */} - + TVL @@ -215,9 +233,9 @@ const PoolItem = React.memo( {pool.tvl} @@ -226,9 +244,9 @@ const PoolItem = React.memo( Max APR @@ -237,9 +255,9 @@ const PoolItem = React.memo( {pool.maxApr} @@ -250,9 +268,9 @@ const PoolItem = React.memo( My Liquidity @@ -261,9 +279,9 @@ const PoolItem = React.memo( {pool.userLiquidity} @@ -279,10 +297,7 @@ const PoolItem = React.memo( ); /** - * Pools Overview Component - * - * @component - * @param {PoolsProps} props - The properties for the Pools component. + * Pools Component */ const Pools = ({ pools, @@ -309,16 +324,32 @@ const Pools = ({ const sortedPools = [...filteredPools]; switch (sort) { case "HighTVL": - sortedPools.sort((a, b) => parseFloat(b.tvl) - parseFloat(a.tvl)); + sortedPools.sort( + (a, b) => + parseFloat(b.tvl.replace(/[^0-9.-]+/g, "")) - + parseFloat(a.tvl.replace(/[^0-9.-]+/g, "")) + ); break; case "LowTVL": - sortedPools.sort((a, b) => parseFloat(a.tvl) - parseFloat(b.tvl)); + sortedPools.sort( + (a, b) => + parseFloat(a.tvl.replace(/[^0-9.-]+/g, "")) - + parseFloat(b.tvl.replace(/[^0-9.-]+/g, "")) + ); break; case "HighAPR": - sortedPools.sort((a, b) => parseFloat(b.maxApr) - parseFloat(a.maxApr)); + sortedPools.sort( + (a, b) => + parseFloat(b.maxApr.replace(/[^0-9.-]+/g, "")) - + parseFloat(a.maxApr.replace(/[^0-9.-]+/g, "")) + ); break; case "LowAPR": - sortedPools.sort((a, b) => parseFloat(a.maxApr) - parseFloat(b.maxApr)); + sortedPools.sort( + (a, b) => + parseFloat(a.maxApr.replace(/[^0-9.-]+/g, "")) - + parseFloat(b.maxApr.replace(/[^0-9.-]+/g, "")) + ); break; default: break; @@ -331,15 +362,15 @@ const Pools = ({ Pools - + onFilterClick("ALL")} @@ -347,19 +378,34 @@ const Pools = ({ selected={filter === "ALL"} /> + + onFilterClick("MY")} + label="My Pools" + selected={filter === "MY"} + /> + - + setSearchValue(e.target.value)} sx={{ width: "100%", - borderRadius: "16px", - border: "1px solid #2D303A", - background: "#1D1F21", - padding: "8px 16px", - lineHeight: "18px", - fontSize: "13px", + borderRadius: borderRadius.lg, + border: `1px solid ${colors.neutral[700]}`, + background: colors.neutral[900], + padding: `${spacing.sm} ${spacing.md}`, + lineHeight: "1.5", + fontSize: typography.fontSize.xs, + color: colors.neutral[300], + transition: "all 0.2s ease", + "&:hover": { + borderColor: colors.neutral[600], + }, + "&:focus-within": { + borderColor: colors.primary.main, + }, "&:before": { content: "none", }, @@ -368,20 +414,21 @@ const Pools = ({ }, }} startAdornment={ - search } /> - + Sort by @@ -400,25 +447,53 @@ const Pools = ({ label="Sort by" value={sort} sx={{ - padding: "16px", - height: "46px", - borderRadius: "16px", - border: "1px solid #2D303A !important", - background: "#1F2123", - fontSize: "14px !important", + padding: `${spacing.sm} ${spacing.md}`, + height: "40px", + borderRadius: borderRadius.lg, + border: `1px solid ${colors.neutral[700]} !important`, + background: colors.neutral[900], + fontSize: `${typography.fontSize.xs} !important`, + color: colors.neutral[300], + transition: "all 0.2s ease", + "&:hover": { + borderColor: colors.neutral[600], + }, + "&.Mui-focused": { + borderColor: colors.primary.main, + }, }} > - TVL High to Low + + TVL High to Low + - TVL Low to High + + TVL Low to High + - APR High to Low + + APR High to Low + - APR Low to High + + APR Low to High + @@ -429,7 +504,7 @@ const Pools = ({ key={index} filter={filter} onAddLiquidityClick={() => onAddLiquidityClick(pool)} - onShowDetailsClick={onShowDetailsClick} + onShowDetailsClick={() => onShowDetailsClick(pool)} pool={pool} /> ))} diff --git a/packages/ui/src/SidebarNavigation/SidebarNavigation.stories.tsx b/packages/ui/src/SidebarNavigation/SidebarNavigation.stories.tsx index 67ff7501..2b4a4647 100644 --- a/packages/ui/src/SidebarNavigation/SidebarNavigation.stories.tsx +++ b/packages/ui/src/SidebarNavigation/SidebarNavigation.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { SidebarNavigation } from "./SidebarNavigation"; import MailIcon from "@mui/icons-material/Mail"; +import React from "react"; // Default metadata of the story https://storybook.js.org/docs/react/api/csf#default-export const meta: Meta = { diff --git a/packages/ui/src/SidebarNavigation/SidebarNavigation.tsx b/packages/ui/src/SidebarNavigation/SidebarNavigation.tsx index 3c32d32f..22feb83d 100644 --- a/packages/ui/src/SidebarNavigation/SidebarNavigation.tsx +++ b/packages/ui/src/SidebarNavigation/SidebarNavigation.tsx @@ -1,4 +1,3 @@ -import Colors from "../Theme/colors"; import React, { useEffect } from "react"; import { CSSObject, styled, Theme } from "@mui/material/styles"; import { @@ -15,7 +14,13 @@ import { useTheme, } from "@mui/material"; import { DrawerProps } from "@phoenix-protocol/types"; -import { motion, MotionProps } from "framer-motion"; // Import MotionProps +import { motion, MotionProps } from "framer-motion"; +import { + colors, + typography, + spacing, + borderRadius, +} from "../Theme/styleConstants"; const drawerWidth = 240; @@ -29,7 +34,7 @@ const openedMixin = (theme: Theme): CSSObject => ({ [theme.breakpoints.down("md")]: { width: "100%", top: "70px", - paddingTop: 2, + paddingTop: spacing.md, }, }); @@ -41,7 +46,7 @@ const closedMixin = (theme: Theme): CSSObject => ({ overflowX: "hidden", width: 0, [theme.breakpoints.up("md")]: { - width: `calc(${theme.spacing(8)} + 1px)`, + width: `calc(${theme.spacing(10)} + 1px)`, // Increased from 8 to 10 }, [theme.breakpoints.down("md")]: { top: "70px", @@ -56,7 +61,6 @@ const DrawerHeader = styled("div")(({ theme }) => ({ justifyContent: "flex-start", })); -// Define props interface for DrawerMotion interface DrawerMotionProps extends MotionProps { open?: boolean; } @@ -103,49 +107,62 @@ const SidebarNavigation = ({ return ( {largerThenMd && ( {open && ( - arrow + )} @@ -153,12 +170,13 @@ const SidebarNavigation = ({ Menu @@ -171,25 +189,34 @@ const SidebarNavigation = ({ open={open} /> {!open && ( - + - arrow + )} {bottomItems && ( - + {item.icon} diff --git a/packages/ui/src/Skeletons/Swap/Swap.tsx b/packages/ui/src/Skeletons/Swap/Swap.tsx index 1ff50627..ccda7026 100644 --- a/packages/ui/src/Skeletons/Swap/Swap.tsx +++ b/packages/ui/src/Skeletons/Swap/Swap.tsx @@ -1,23 +1,26 @@ import { Box, Skeleton, Typography, List, ListItem } from "@mui/material"; import React from "react"; +import { colors, typography, spacing, borderRadius } from "../../Theme/styleConstants"; const listItemContainer = { display: "flex", justifyContent: "space-between", - padding: "8px 0", + padding: `${spacing.xs} 0`, }; const listItemNameStyle = { - color: "var(--content-medium-emphasis, rgba(255, 255, 255, 0.70))", - fontSize: "14px", + color: "rgba(255, 255, 255, 0.70)", + fontSize: typography.fontSize.sm, + fontFamily: typography.fontFamily, lineHeight: "140%", marginBottom: 0, }; const listItemContentStyle = { - color: "#FFF", - fontSize: "14px", - fontWeight: "700", + color: colors.neutral[50], + fontSize: typography.fontSize.sm, + fontFamily: typography.fontFamily, + fontWeight: typography.fontWeights.bold, lineHeight: "140%", }; @@ -28,7 +31,7 @@ export const Swap = () => { width: "100%", display: "flex", flexDirection: "column", - gap: "16px", + gap: spacing.md, }} > {/* Header Section */} @@ -37,34 +40,76 @@ export const Swap = () => { display: "flex", justifyContent: "space-between", alignItems: "center", + background: colors.neutral[900], + borderRadius: borderRadius.lg, + border: `1px solid ${colors.neutral[700]}`, + padding: spacing.md, }} > Swap tokens instantly - + {/* Main Content Section */} {/* Swap Form Section */} - - - - + + + + {/* Swap Details Section */} @@ -72,18 +117,19 @@ export const Swap = () => { sx={{ flex: 1, width: "100%", - padding: "24px", - borderRadius: "12px", - border: "1px solid var(--Secondary-S4, #2C2C31)", - background: - "var(--Secondary-S3, linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.03) 100%))", + padding: spacing.lg, + borderRadius: borderRadius.lg, + border: `1px solid ${colors.neutral[700]}`, + background: colors.neutral[900], }} > Swap Details @@ -97,25 +143,41 @@ export const Swap = () => { Exchange rate - + Protocol fee - + Route - + Slippage tolerance - + diff --git a/packages/ui/src/Swap/AssetSelector/AssetItem.tsx b/packages/ui/src/Swap/AssetSelector/AssetItem.tsx index e2e470b7..1561e98b 100644 --- a/packages/ui/src/Swap/AssetSelector/AssetItem.tsx +++ b/packages/ui/src/Swap/AssetSelector/AssetItem.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Box, Button } from "@mui/material"; import { Token } from "@phoenix-protocol/types"; +import { colors, typography, spacing, borderRadius } from "../../Theme/styleConstants"; /** * AssetItem @@ -26,19 +27,19 @@ const AssetItem = ({ alignItems: "center", justifyContent: "flex-start", width: "100%", - padding: "8px 12px", - borderRadius: "8px", - background: - "linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.03) 100%)", - color: "white", + padding: `${spacing.xs} ${spacing.md}`, + borderRadius: borderRadius.sm, + background: colors.neutral[900], + border: `1px solid ${colors.neutral[700]}`, + color: colors.neutral[300], textTransform: "none", - fontSize: "14px", - fontWeight: 500, - lineHeight: "1.5", + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeights.medium, + fontFamily: typography.fontFamily, + lineHeight: 1.5, mb: 1, "&:hover": { - background: - "linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%)", + background: colors.neutral[800], }, }} > @@ -49,7 +50,8 @@ const AssetItem = ({ sx={{ width: "24px", height: "24px", - marginRight: "12px", + marginRight: spacing.md, + opacity: 0.7, }} /> {token.name} diff --git a/packages/ui/src/Swap/AssetSelector/AssetSelector.tsx b/packages/ui/src/Swap/AssetSelector/AssetSelector.tsx index 20db8b17..f9099733 100644 --- a/packages/ui/src/Swap/AssetSelector/AssetSelector.tsx +++ b/packages/ui/src/Swap/AssetSelector/AssetSelector.tsx @@ -1,18 +1,18 @@ import React, { useState, useMemo } from "react"; -import { Box, Grid, IconButton, Input, Typography } from "@mui/material"; +import { Box, Grid, IconButton, Typography } from "@mui/material"; import { motion } from "framer-motion"; import { KeyboardArrowLeft } from "@mui/icons-material"; import { AssetSelectorProps } from "@phoenix-protocol/types"; import AssetItem from "./AssetItem"; +import { SearchInput } from "../../Common/SearchInput"; +import { colors, typography, spacing, borderRadius, shadows } from "../../Theme/styleConstants"; +import { CardContainer } from "../../Common/CardContainer"; /** * AssetSelector * A modern and searchable token selector modal with quick select and token list sections. - * - * @param {AssetSelectorProps} props - Props containing token data and event handlers. - * @returns {JSX.Element} */ -const AssetSelector = ({ +export const AssetSelector = ({ tokens, tokensAll, onClose, @@ -38,13 +38,10 @@ const AssetSelector = ({ exit={{ opacity: 0 }} transition={{ duration: 0.3, ease: "easeInOut" }} > - {/* Header */} @@ -52,26 +49,30 @@ const AssetSelector = ({ sx={{ display: "flex", alignItems: "center", - marginBottom: "1rem", + marginBottom: spacing.md, }} > - + Select Token @@ -84,29 +85,11 @@ const AssetSelector = ({ animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }} > - ) => - setSearchValue(e.target.value) - } - sx={{ - width: "100%", - borderRadius: "16px", - border: "1px solid #2D303A", - background: "#1D1F21", - padding: "8px 16px", - color: "white", - fontSize: "14px", - marginBottom: "16px", - "&:before, &:after": { content: "none" }, - }} - startAdornment={ - Search - } + value={searchValue} + onChange={(e) => setSearchValue(e.target.value)} + sx={{ marginBottom: spacing.md }} /> @@ -117,21 +100,14 @@ const AssetSelector = ({ animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3, delay: 0.2 }} > - + Quick select @@ -143,25 +119,19 @@ const AssetSelector = ({ ))} - + )} {/* All Tokens Section */} - + All tokens @@ -174,8 +144,8 @@ const AssetSelector = ({ width: "6px", }, "&::-webkit-scrollbar-thumb": { - backgroundColor: "#E2491A", - borderRadius: "8px", + backgroundColor: colors.primary.main, + borderRadius: borderRadius.sm, }, }} > @@ -189,31 +159,30 @@ const AssetSelector = ({ display: "flex", flexDirection: "column", alignItems: "center", - padding: "1rem", + padding: spacing.md, }} > - We didn’t find any assets for "{searchValue}" + We didn't find any assets for "{searchValue}" )} - - + + ); }; - -export { AssetSelector }; diff --git a/packages/ui/src/Swap/SlippageSettings/SlippageSettings.stories.tsx b/packages/ui/src/Swap/SlippageSettings/SlippageSettings.stories.tsx index b72455af..b50669c3 100644 --- a/packages/ui/src/Swap/SlippageSettings/SlippageSettings.stories.tsx +++ b/packages/ui/src/Swap/SlippageSettings/SlippageSettings.stories.tsx @@ -16,7 +16,7 @@ type Story = StoryObj; export const Primary: Story = { args: { - options: ["1%", "3%", "5%"], + options: [1, 3, 5], selectedOption: 1, }, }; diff --git a/packages/ui/src/Swap/SlippageSettings/SlippageSettings.tsx b/packages/ui/src/Swap/SlippageSettings/SlippageSettings.tsx index b13bf2e6..d48b4194 100644 --- a/packages/ui/src/Swap/SlippageSettings/SlippageSettings.tsx +++ b/packages/ui/src/Swap/SlippageSettings/SlippageSettings.tsx @@ -1,22 +1,25 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { Alert, Box, FormControl, - FormControlLabel, - FormLabel, Grid, IconButton, - Input, InputAdornment, - Radio, - RadioGroup, TextField, Typography, + Slider, + Tooltip, } from "@mui/material"; -import { KeyboardArrowLeft } from "@mui/icons-material"; -import Colors from "../../Theme/colors"; +import { KeyboardArrowLeft, InfoOutlined } from "@mui/icons-material"; +import { + colors, + typography, + spacing, + borderRadius, +} from "../../Theme/styleConstants"; import { SlippageOptionsProps } from "@phoenix-protocol/types"; +import { motion, AnimatePresence } from "framer-motion"; const SlippageSettings = ({ options, @@ -24,28 +27,141 @@ const SlippageSettings = ({ onClose, onChange, }: SlippageOptionsProps) => { - const [customInputValue, setCustomInputValue] = React.useState(""); + const [customInputValue, setCustomInputValue] = useState( + selectedOption + ); + const [activeOption, setActiveOption] = useState( + options.includes(selectedOption) ? selectedOption : null + ); + const [customValue, setCustomValue] = useState(false); + const [showWarning, setShowWarning] = useState(false); + + // Set initial values on component mount or when selectedOption changes + useEffect(() => { + setCustomInputValue(selectedOption); + setActiveOption(options.includes(selectedOption) ? selectedOption : null); + }, [selectedOption, options]); + + // Check if slippage is high or too low + const evaluateSlippage = (value: number) => { + if (value > 5) { + return "high"; + } else if (value < 0.5) { + return "low"; + } + return "normal"; + }; + + const slippageStatus = evaluateSlippage(customInputValue); - const handleChange = (optionValue: string) => { - onChange(optionValue !== "custom" ? optionValue : customInputValue); + const handleOptionClick = (optionValue: number) => { + setActiveOption(optionValue); + setCustomInputValue(optionValue); + onChange(optionValue); }; const handleCustomInputChange = (event) => { - const value = Number(event.target.value) > 30 ? "30" : event.target.value; + const value = event.target.value; + + // Validate the input - allow both commas and dots + if (value === "" || /^\d*[.,]?\d*$/.test(value)) { + // Convert commas to dots for proper number parsing + const normalizedValue = value.replace(",", "."); + const numericValue = Number(normalizedValue); + + // If input matches an option, highlight that option + const matchedOption = options.includes(numericValue) + ? numericValue + : null; + setActiveOption(matchedOption); + + if (numericValue > 30) { + setCustomInputValue(30); + onChange(30); + setShowWarning(true); + } else { + // Set the input field to show the original input (with comma or dot) + setCustomInputValue(normalizedValue === "" ? null : numericValue); + onChange(numericValue || 0); + setShowWarning(numericValue > 5); + } + } + }; + + const getSlippageColor = () => { + switch (slippageStatus) { + case "high": + return colors.warning.main; + case "low": + return colors.error.main; + default: + return colors.success[300]; + } + }; - setCustomInputValue(value); - handleChange(value); + const getSlippageMessage = () => { + switch (slippageStatus) { + case "high": + return "High slippage tolerance may lead to unfavorable trades"; + case "low": + return "Transaction may fail due to price movements"; + default: + return null; + } }; + const slippageMessage = getSlippageMessage(); + + const OptionButton = ({ value, isSelected, onClick }) => ( + + onClick(value)} + sx={{ + padding: `${spacing.xs} ${spacing.sm}`, + borderRadius: borderRadius.md, + background: isSelected + ? `rgba(${colors.primary.gradient}, 0.1)` + : colors.neutral[900], + border: `1px solid ${ + isSelected ? colors.primary.main : colors.neutral[700] + }`, + cursor: "pointer", + textAlign: "center", + transition: "all 0.2s ease", + "&:hover": { + borderColor: colors.primary.main, + }, + }} + > + + {value} + + + + ); + return ( - Settings + Slippage Settings - - + Slippage Tolerance + + + + + + + + + Set the maximum price slippage you're willing to accept for your + trades + + + {/* Slippage options */} + + {options.map((option) => ( + + + + ))} + + + {/* Custom option */} + + setCustomValue(true)} + onFocus={() => setCustomValue(true)} + placeholder="Custom" + type="number" + InputProps={{ + endAdornment: ( + + % + + ), + sx: { + color: colors.neutral[50], + fontSize: typography.fontSize.md, + fontWeight: typography.fontWeights.medium, + borderRadius: borderRadius.md, + background: customValue + ? `rgba(${colors.primary.gradient}, 0.05)` + : "transparent", + border: `1px solid ${ + customValue ? colors.primary.main : colors.neutral[700] + }`, + "& .MuiOutlinedInput-notchedOutline": { + border: "none", + }, + "&:hover": { + borderColor: colors.primary.main, + }, + transition: "all 0.2s ease", + }, + }} + /> + + {/* Visual slippage indicator */} + - Select Spread tolerance - - handleChange(e.target.value)} - > - {options.map((option, index) => ( - - } - label={ - - {option} - - } - /> - ))} - + + + + {/* Warning message */} + + {slippageMessage && ( + + + - } - label={ - % - ), - sx: { - minWidth: "140px", - color: "white", - fontSize: "14px", - lineHeight: "16px", - borderRadius: "16px", - "&:hover fieldset": { - border: "1px solid #E2621B!important", - }, - "&:focus-within fieldset, &:focus-visible fieldset": { - border: "2px solid #E2621B!important", - color: "white!important", - }, - }, - }} - /> - } - /> - - + > + {slippageMessage} + + + + )} + ); diff --git a/packages/ui/src/Swap/SwapContainer/SwapAssetsButton.stories.tsx b/packages/ui/src/Swap/SwapContainer/SwapAssetsButton.stories.tsx new file mode 100644 index 00000000..841fb7c6 --- /dev/null +++ b/packages/ui/src/Swap/SwapContainer/SwapAssetsButton.stories.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { StoryFn, Meta } from "@storybook/react"; +import { SwapAssetsButton } from "./SwapAssetsButton"; +import { Box } from "@mui/material"; + +export default { + title: "Swap/SwapAssetsButton", + component: SwapAssetsButton, + parameters: { + layout: "centered", + }, +} as Meta; + +const Template: StoryFn = (args) => ( + + + +); + +export const Default = Template.bind({}); +Default.args = { + onClick: () => console.log("Swap assets clicked"), +}; diff --git a/packages/ui/src/Swap/SwapContainer/SwapAssetsButton.tsx b/packages/ui/src/Swap/SwapContainer/SwapAssetsButton.tsx new file mode 100644 index 00000000..5da5aaef --- /dev/null +++ b/packages/ui/src/Swap/SwapContainer/SwapAssetsButton.tsx @@ -0,0 +1,54 @@ +import React, { useState } from "react"; +import { motion } from "framer-motion"; +import { Button } from "../../Button/Button"; +import { colors, borderRadius, shadows } from "../../Theme/styleConstants"; + +interface SwapAssetsButtonProps { + onClick: () => void; +} + +export const SwapAssetsButton = ({ onClick }: SwapAssetsButtonProps) => { + const [isSpinning, setIsSpinning] = useState(false); + + const handleClick = () => { + if (!isSpinning) { + onClick(); + setIsSpinning(true); + setTimeout(() => setIsSpinning(false), 1000); // Reset spinning animation after 1 second + } + }; + + return ( + + ); +}; diff --git a/packages/ui/src/Swap/SwapContainer/SwapContainer.tsx b/packages/ui/src/Swap/SwapContainer/SwapContainer.tsx index e12f2216..f60d03c9 100644 --- a/packages/ui/src/Swap/SwapContainer/SwapContainer.tsx +++ b/packages/ui/src/Swap/SwapContainer/SwapContainer.tsx @@ -1,76 +1,13 @@ -import React, { useState } from "react"; -import { Box, IconButton, List, ListItem, Typography } from "@mui/material"; +import React from "react"; +import { Box, List, ListItem, Typography } from "@mui/material"; import { motion } from "framer-motion"; -import { ExpandMore as ExpandMoreIcon } from "@mui/icons-material"; import { TokenBox } from "../TokenBox/TokenBox"; import { Button } from "../../Button/Button"; import { SwapContainerProps } from "@phoenix-protocol/types"; - -const listItemContainer = { - display: "flex", - justifyContent: "space-between", - padding: "8px 0", -}; - -const listItemNameStyle = { - color: "var(--content-medium-emphasis, rgba(255, 255, 255, 0.70))", - fontSize: "14px", - lineHeight: "140%", - marginBottom: 0, -}; - -const listItemContentStyle = { - color: "#FFF", - fontSize: "14px", - fontWeight: "700", - lineHeight: "140%", -}; - -const SwapAssetsButton = ({ onClick }: { onClick: () => void }) => { - const [isSpinning, setIsSpinning] = useState(false); - - const handleClick = () => { - if (!isSpinning) { - onClick(); - setIsSpinning(true); - setTimeout(() => setIsSpinning(false), 1000); // Reset spinning animation after 1 second - } - }; - - return ( - - ); -}; +import { SwapAssetsButton } from "./SwapAssetsButton"; +import { CardContainer } from "../../Common/CardContainer"; +import { SwapDetails } from "./SwapDetails"; +import { colors, typography, spacing, borderRadius } from "../../Theme/styleConstants"; const SwapContainer = ({ fromToken, @@ -105,49 +42,35 @@ const SwapContainer = ({ width: "100%", display: "flex", flexDirection: "column", - gap: "16px", + gap: spacing.md, + alignItems: "center", }} > {/* Header Section */} - + Swap tokens instantly - - Options - - + {/* Main Content Section */} {/* Swap Form Section */} - +
@@ -186,23 +109,34 @@ const SwapContainer = ({ loadingValues={loadingSimulate} />
+ + Adjust maximum spread + {trustlineButtonActive ? ( - <> - + /> + { sx={{ "& .MuiMenu-paper": { display: "flex", - padding: "0.75rem 1.5rem", + padding: `${spacing.sm} ${spacing.lg}`, flexDirection: "column", - gap: "1.5rem", - borderRadius: "0.5rem", - border: "1px solid #292B2C", - background: "linear-gradient(180deg, #292B2C 0%, #1F2123 100%)", - boxShadow: - "-3px 3px 10px 0px rgba(25, 13, 1, 0.10), -12px 13px 18px 0px rgba(25, 13, 1, 0.09), -26px 30px 24px 0px rgba(25, 13, 1, 0.05), -46px 53px 28px 0px rgba(25, 13, 1, 0.02), -73px 83px 31px 0px rgba(25, 13, 1, 0.00)", + gap: spacing.lg, + borderRadius: borderRadius.md, + border: `1px solid ${colors.neutral[700]}`, + background: colors.neutral[800], + color: colors.neutral[300], + boxShadow: shadows.tooltip, }, }} > @@ -161,45 +144,11 @@ const FilterMenu = ({ activeFilters, applyFilters }: FilterMenuProps) => { display: "flex", justifyContent: "space-between", alignItems: "center", - }} - > - - DATE RANGE - - - - - From - { sx={{ "& .MuiInputBase-root": { alignItems: "center", - color: "rgba(255, 255, 255, 0.70)", - fontFamily: "Ubuntu", - fontSize: "0.875rem", - fontStyle: "normal", - fontWeight: 400, - borderRadius: "1rem", + color: colors.neutral[300], + fontFamily: typography.fontFamily, + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeights.regular, + borderRadius: borderRadius.lg, minWidth: "unset", + background: colors.neutral[900], + border: `1px solid ${colors.neutral[700]}`, }, "& legend": { display: "none", }, "& .MuiOutlinedInput-notchedOutline": { - border: "1px solid #2D303A", - top: "-2px", + border: "none", }, }} /> @@ -246,12 +195,11 @@ const FilterMenu = ({ activeFilters, applyFilters }: FilterMenuProps) => { @@ -271,7 +219,6 @@ const FilterMenu = ({ activeFilters, applyFilters }: FilterMenuProps) => { onChange={(newValue: dayjs.Dayjs | null) => setDateRange({ ...dateRange, - // @ts-ignore to: newValue === null ? undefined : newValue.toDate(), }) } @@ -282,13 +229,14 @@ const FilterMenu = ({ activeFilters, applyFilters }: FilterMenuProps) => { sx={{ "& .MuiInputBase-root": { alignItems: "center", - color: "rgba(255, 255, 255, 0.70)", - fontFamily: "Ubuntu", - fontSize: "0.875rem", - fontStyle: "normal", - fontWeight: 400, - borderRadius: "1rem", + color: colors.neutral[300], + fontFamily: typography.fontFamily, + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeights.regular, + borderRadius: borderRadius.lg, minWidth: "unset", + background: colors.neutral[900], + border: `1px solid ${colors.neutral[700]}`, }, "& .MuiTextField-root": { minWidth: "0 !important", @@ -297,8 +245,7 @@ const FilterMenu = ({ activeFilters, applyFilters }: FilterMenuProps) => { display: "none", }, "& .MuiOutlinedInput-notchedOutline": { - border: "1px solid #2D303A", - top: "-2px", + border: "none", }, }} /> @@ -306,393 +253,10 @@ const FilterMenu = ({ activeFilters, applyFilters }: FilterMenuProps) => { - - - - TRADE SIZE - - - - - - - From - - - - setTradeSize({ - ...tradeSize, - from: parseInt(e.target.value), - }) - } - sx={{ - marginTop: "0.5rem", - width: "100%", - "& .MuiInputBase-root": { - margin: 0, - alignItems: "center", - color: "rgba(255, 255, 255, 0.70)", - fontFamily: "Ubuntu", - fontSize: "0.875rem", - fontStyle: "normal", - fontWeight: 400, - borderRadius: "1rem", - "& .MuiOutlinedInput-input": { - padding: "0.5rem 0.75rem 0.75rem 0.75rem", - margin: 0, - }, - }, - "& legend": { - display: "none", - }, - "& .MuiOutlinedInput-notchedOutline": { - border: "1px solid #2D303A", - }, - }} - /> - - - - To - - - setTradeSize({ - ...tradeSize, - to: parseInt(e.target.value), - }) - } - fullWidth - sx={{ - marginTop: "0.5rem", - width: "100%", - "& .MuiInputBase-root": { - margin: 0, - alignItems: "center", - color: "rgba(255, 255, 255, 0.70)", - fontFamily: "Ubuntu", - fontSize: "0.875rem", - fontStyle: "normal", - fontWeight: 400, - borderRadius: "1rem", - "& .MuiOutlinedInput-input": { - padding: "0.5rem 0.75rem 0.75rem 0.75rem", - margin: 0, - }, - }, - "& legend": { - display: "none", - }, - "& .MuiOutlinedInput-notchedOutline": { - border: "1px solid #2D303A", - }, - }} - /> - - - - - - TRADE VALUE - - - - - - - From - - - - setTradeValue({ - ...tradeValue, - from: parseInt(e.target.value), - }) - } - sx={{ - marginTop: "0.5rem", - width: "100%", - "& .MuiInputBase-root": { - margin: 0, - alignItems: "center", - color: "rgba(255, 255, 255, 0.70)", - fontFamily: "Ubuntu", - fontSize: "0.875rem", - fontStyle: "normal", - fontWeight: 400, - borderRadius: "1rem", - "& .MuiOutlinedInput-input": { - padding: "0.5rem 0.75rem 0.75rem 0.75rem", - margin: 0, - }, - }, - "& legend": { - display: "none", - }, - "& .MuiOutlinedInput-notchedOutline": { - border: "1px solid #2D303A", - }, - }} - /> - - - - To - - - setTradeValue({ - ...tradeValue, - to: parseInt(e.target.value), - }) - } - fullWidth - sx={{ - marginTop: "0.5rem", - width: "100%", - "& .MuiInputBase-root": { - margin: 0, - alignItems: "center", - color: "rgba(255, 255, 255, 0.70)", - fontFamily: "Ubuntu", - fontSize: "0.875rem", - fontStyle: "normal", - fontWeight: 400, - borderRadius: "1rem", - "& .MuiOutlinedInput-input": { - padding: "0.5rem 0.75rem 0.75rem 0.75rem", - margin: 0, - }, - }, - "& legend": { - display: "none", - }, - "& .MuiOutlinedInput-notchedOutline": { - border: "1px solid #2D303A", - }, - }} - /> - - - - updateFilters()} /> - - resetFilters()} - /> - + + + + ); diff --git a/packages/ui/src/Transactions/TransactionsTable/TransactionEntry.tsx b/packages/ui/src/Transactions/TransactionsTable/TransactionEntry.tsx index 12f9b9a7..1ff1a3cc 100644 --- a/packages/ui/src/Transactions/TransactionsTable/TransactionEntry.tsx +++ b/packages/ui/src/Transactions/TransactionsTable/TransactionEntry.tsx @@ -1,8 +1,8 @@ import React from "react"; import { Box, Grid, Typography, useMediaQuery } from "@mui/material"; -import { ArrowForward, ArrowBack } from "@mui/icons-material"; -import { TransactionTableEntryProps } from "@phoenix-protocol/types"; +import { ArrowForward } from "@mui/icons-material"; import LaunchIcon from "@mui/icons-material/Launch"; +import { TransactionTableEntryProps } from "@phoenix-protocol/types"; const TransactionEntry = ( props: TransactionTableEntryProps & { isMobile: boolean } @@ -10,9 +10,9 @@ const TransactionEntry = ( const { isMobile } = props; // Destructure isMobile const BoxStyle = { p: 3, - borderRadius: "8px", - background: - "linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.03) 100%)", + borderRadius: "12px", // Adjusted border radius + background: "var(--neutral-900, #171717)", // Adjusted background + border: "1px solid var(--neutral-700, #404040)", // Adjusted border position: "relative", overflow: "hidden", boxShadow: "2px 2px 4px rgba(0, 0, 0, 0.3)", @@ -64,6 +64,7 @@ const TransactionEntry = ( fontSize: "12px", fontWeight: "700", textTransform: "uppercase", + color: "var(--neutral-400, #A3A3A3)", // Adjusted color opacity: 0.6, }} > @@ -72,7 +73,7 @@ const TransactionEntry = ( )} @@ -106,20 +108,25 @@ const TransactionEntry = ( component="img" src={props.fromAsset.icon} alt={props.fromAsset.name} - sx={{ width: "20px", height: "20px", mr: "0.5rem" }} + sx={{ + width: "20px", + height: "20px", + mr: "0.5rem", + opacity: 0.7, + }} // Adjusted opacity /> {props.fromAmount} {props.toAmount} @@ -187,6 +202,7 @@ const TransactionEntry = ( sx={{ fontSize: isMobile ? "12px" : "14px", fontWeight: "400", + color: "var(--neutral-300, #D4D4D4)", // Adjusted color }} > ${props.tradeValue} @@ -199,6 +215,7 @@ const TransactionEntry = ( fontSize: "12px", fontWeight: "700", textTransform: "uppercase", + color: "var(--neutral-400, #A3A3A3)", // Adjusted color opacity: 0.6, }} > @@ -219,14 +236,19 @@ const TransactionEntry = ( fontWeight: "400", textDecoration: "underline", textDecorationStyle: "dotted", + color: "var(--neutral-300, #D4D4D4)", // Adjusted color "&:hover": { textDecoration: "underline", cursor: "pointer", }, }} > - {props.txHash} diff --git a/packages/ui/src/Transactions/TransactionsTable/TransactionsHeader.tsx b/packages/ui/src/Transactions/TransactionsTable/TransactionsHeader.tsx index 09f31286..466b02d4 100644 --- a/packages/ui/src/Transactions/TransactionsTable/TransactionsHeader.tsx +++ b/packages/ui/src/Transactions/TransactionsTable/TransactionsHeader.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Box, Typography } from "@mui/material"; import { ArrowDownward, SwapVert } from "@mui/icons-material"; +import { colors, typography } from "../../Theme/styleConstants"; function convertToCamelCase(input: string): string { return input @@ -31,10 +32,11 @@ const TransactionHeader = ({ > ) : ( - + ))} ); diff --git a/packages/ui/src/Transactions/TransactionsTable/TransactionsTable.stories.tsx b/packages/ui/src/Transactions/TransactionsTable/TransactionsTable.stories.tsx index 66f1d7b1..8b3d4ae2 100644 --- a/packages/ui/src/Transactions/TransactionsTable/TransactionsTable.stories.tsx +++ b/packages/ui/src/Transactions/TransactionsTable/TransactionsTable.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { TransactionsTable } from "./TransactionsTable"; +import React from "react"; // Default metadata of the story https://storybook.js.org/docs/react/api/csf#default-export const meta: Meta = { diff --git a/packages/ui/src/Transactions/TransactionsTable/TransactionsTable.tsx b/packages/ui/src/Transactions/TransactionsTable/TransactionsTable.tsx index acb897d8..2b10f2fd 100644 --- a/packages/ui/src/Transactions/TransactionsTable/TransactionsTable.tsx +++ b/packages/ui/src/Transactions/TransactionsTable/TransactionsTable.tsx @@ -6,21 +6,27 @@ import { TransactionsTableProps } from "@phoenix-protocol/types"; import TransactionEntry from "./TransactionEntry"; import TransactionHeader from "./TransactionsHeader"; import { maxWidth } from "@mui/system"; +import { + colors, + typography, + spacing, + borderRadius, +} from "../../Theme/styleConstants"; const customSpacing = { - xs: "8px", - sm: "12px", - md: "16px", + xs: spacing.xs, + sm: spacing.sm, + md: spacing.md, }; const classes = { root: { marginTop: customSpacing.md, padding: `${customSpacing.md} ${customSpacing.md}`, - borderRadius: "8px", - background: - "linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.03) 100%)", + borderRadius: borderRadius.md, + background: colors.neutral[900], overflowX: "auto", + border: `1px solid ${colors.neutral[700]}`, }, tabUnselected: { display: "flex", @@ -30,19 +36,19 @@ const classes = { justifyContent: "center", alignItems: "center", gap: "0.625rem", - borderRadius: "1rem", + borderRadius: borderRadius.lg, cursor: "pointer", - background: - "linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.03) 100%)", - color: "#FFF", + color: colors.neutral[300], + background: colors.neutral[900], opacity: 0.6, textAlign: "center", fontFeatureSettings: "'clig' off, 'liga' off", - fontFamily: "Ubuntu", - fontSize: "0.625rem", + fontFamily: typography.fontFamily, + fontSize: typography.fontSize.xs, fontStyle: "normal", - fontWeight: 700, - lineHeight: "1.25rem", // 200% + fontWeight: typography.fontWeights.bold, + lineHeight: "1.25rem", + border: `1px solid ${colors.neutral[700]}`, }, tabSelected: { display: "flex", @@ -52,17 +58,17 @@ const classes = { alignItems: "center", gap: "0.625rem", flex: "1 0 0", - borderRadius: "1rem", - border: "1px solid var(--Primary-P3, #E2571C)", - background: "rgba(226, 73, 26, 0.10)", - color: "#FFF", + borderRadius: borderRadius.lg, + border: `1px solid ${colors.primary.main}`, + background: `rgba(${colors.primary.gradient}, 0.10)`, + color: colors.neutral[50], textAlign: "center", fontFeatureSettings: "'clig' off, 'liga' off", - fontFamily: "Ubuntu", - fontSize: "0.625rem", + fontFamily: typography.fontFamily, + fontSize: typography.fontSize.xs, fontStyle: "normal", - fontWeight: 700, - lineHeight: "1.25rem", // 200% + fontWeight: typography.fontWeights.bold, + lineHeight: "1.25rem", }, }; @@ -157,8 +163,8 @@ const TransactionsTable = ({ style={{ padding: `${customSpacing.md} ${customSpacing.md}`, borderRadius: "8px", - background: - "linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.03) 100%)", + background: colors.neutral[900], + border: `1px solid ${colors.neutral[700]}`, boxShadow: "2px 2px 4px rgba(0, 0, 0, 0.3)", }} > @@ -222,7 +228,7 @@ const TransactionsTable = ({ = { diff --git a/packages/ui/src/Transactions/VolumeChart/VolumeChart.tsx b/packages/ui/src/Transactions/VolumeChart/VolumeChart.tsx index 96028d9b..f585b868 100644 --- a/packages/ui/src/Transactions/VolumeChart/VolumeChart.tsx +++ b/packages/ui/src/Transactions/VolumeChart/VolumeChart.tsx @@ -1,4 +1,4 @@ -import { Box, Typography, MenuItem, Select, TextField } from "@mui/material"; +import { Box, Typography, MenuItem, TextField } from "@mui/material"; import React, { useMemo, useRef, useState } from "react"; import { BarChart, @@ -11,6 +11,8 @@ import { ResponsiveContainer, } from "recharts"; import { formatCurrencyStatic } from "@phoenix-protocol/utils"; +import { CustomDropdown } from "../../Common/CustomDropdown"; +import { colors, typography, spacing, borderRadius } from "../../Theme/styleConstants"; type Pool = { tokenA: { icon: string; symbol: string }; @@ -78,16 +80,17 @@ const tabUnselectedStyles = { justifyContent: "center", alignItems: "center", gap: "0.625rem", - borderRadius: "1rem", + borderRadius: borderRadius.md, cursor: "pointer", - background: - "var(--Secondary-S3, linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.03) 100%))", + color: colors.neutral[300], + background: colors.neutral[900], + border: `1px solid ${colors.neutral[700]}`, }; const tabSelectedStyles = { - borderRadius: "1rem", - border: "1px solid #E2571C", + borderRadius: borderRadius.md, background: "rgba(226, 73, 26, 0.10)", + color: colors.neutral[50], }; const VolumeChart = ({ @@ -131,8 +134,8 @@ const VolumeChart = ({ alignItems: "flex-start", gap: "1.5625rem", borderRadius: "1.5rem", - background: - "linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.03) 100%)", + background: colors.neutral[900], // Adjusted background + border: `1px solid ${colors.neutral[700]}`, // Adjusted border }} > Volume {resolveSelectedVolume(selectedTab)} (USD) - + {formatCurrencyStatic.format(totalVolume)} @@ -317,7 +217,7 @@ const VolumeChart = ({
- + @@ -339,12 +239,11 @@ const VolumeChart = ({ - { if (active && payload && payload.length) { diff --git a/packages/ui/src/UnstakeInfoModal/UnstakeInfoModal.tsx b/packages/ui/src/UnstakeInfoModal/UnstakeInfoModal.tsx index 0d363779..8f20c53f 100644 --- a/packages/ui/src/UnstakeInfoModal/UnstakeInfoModal.tsx +++ b/packages/ui/src/UnstakeInfoModal/UnstakeInfoModal.tsx @@ -6,9 +6,9 @@ import { Grid, CircularProgress, } from "@mui/material"; -import Colors from "../Theme/colors"; import { Button } from "../Button/Button"; import { UnstakeInfoModalProps } from "@phoenix-protocol/types"; +import { colors, typography, spacing, borderRadius, shadows } from "../Theme/styleConstants"; const UnstakeInfoModal = ({ open, @@ -22,11 +22,12 @@ const UnstakeInfoModal = ({ transform: "translate(-50%, -50%)", width: 512, maxWidth: "calc(100vw - 16px)", - background: "linear-gradient(180deg, #292B2C 0%, #1F2123 100%)", - borderRadius: "16px", + background: colors.neutral[900], + borderRadius: borderRadius.lg, display: "flex", flexDirection: "column" as "column", - padding: "16px", + padding: spacing.lg, + boxShadow: shadows.card, }; return ( @@ -53,10 +54,10 @@ const UnstakeInfoModal = ({ display: "inline-flex", justifyContent: "center", alignItems: "center", - w: "16px", - h: "16px", - backgroundColor: Colors.inputsHover, - borderRadius: "8px", + width: "16px", + height: "16px", + backgroundColor: colors.neutral[800], + borderRadius: borderRadius.sm, cursor: "pointer", }} src="/x.svg" @@ -72,39 +73,45 @@ const UnstakeInfoModal = ({ > Warning - - - - You{"'"}re about to unstake. By doing so, you will lose the APR - progress you've accumulated. Remember, your APR increases daily - up to 60 days. If you unstake now, you'll need to start over - from day one. -
-
- Are you sure you want to proceed? -
-