page.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. 'use client'
  2. import { useState } from 'react'
  3. import {
  4. Card,
  5. Input,
  6. Button,
  7. Form,
  8. InputNumber,
  9. message,
  10. Descriptions,
  11. Typography,
  12. Space,
  13. } from 'antd'
  14. import { CopyOutlined, LoadingOutlined } from '@ant-design/icons'
  15. const { Title, Text } = Typography
  16. export default function LpCopyPage() {
  17. const [loading, setLoading] = useState(false)
  18. const [positionInfo, setPositionInfo] = useState<{
  19. poolAddress: string
  20. tickLower: number
  21. tickUpper: number
  22. base: string
  23. baseAmount: string
  24. otherAmountMax: string
  25. estimatedValue: number
  26. priceLower: string
  27. priceUpper: string
  28. } | null>(null)
  29. const [form] = Form.useForm()
  30. const handleCopy = async (values: {
  31. positionAddress: string
  32. maxUsdValue: number
  33. }) => {
  34. setLoading(true)
  35. setPositionInfo(null)
  36. try {
  37. const response = await fetch('/api/lp-copy', {
  38. method: 'POST',
  39. headers: {
  40. 'Content-Type': 'application/json',
  41. },
  42. body: JSON.stringify({
  43. positionAddress: values.positionAddress,
  44. maxUsdValue: values.maxUsdValue,
  45. }),
  46. })
  47. const data = await response.json()
  48. if (!response.ok) {
  49. throw new Error(data.error || 'Failed to copy position')
  50. }
  51. setPositionInfo(data.positionInfo)
  52. message.success('Position copied successfully!')
  53. message.info(`Transaction ID: ${data.txid}`)
  54. } catch (error: unknown) {
  55. const errorMessage =
  56. error instanceof Error ? error.message : 'Failed to copy position'
  57. message.error(errorMessage)
  58. } finally {
  59. setLoading(false)
  60. }
  61. }
  62. return (
  63. <div style={{ padding: '24px', maxWidth: '800px', margin: '0 auto' }}>
  64. <Title level={2}>LP Position Copy</Title>
  65. <Text type="secondary">
  66. Copy an existing LP position with a limited investment amount. The
  67. system will automatically calculate the optimal token amounts.
  68. </Text>
  69. <Card style={{ marginTop: '24px' }}>
  70. <Form
  71. form={form}
  72. layout="vertical"
  73. onFinish={handleCopy}
  74. initialValues={{
  75. maxUsdValue: 10,
  76. }}
  77. >
  78. <Form.Item
  79. label="Position Address"
  80. name="positionAddress"
  81. rules={[
  82. { required: true, message: 'Please enter position address' },
  83. {
  84. pattern: /^[1-9A-HJ-NP-Za-km-z]{32,44}$/,
  85. message: 'Invalid Solana address format',
  86. },
  87. ]}
  88. >
  89. <Input
  90. placeholder="Enter the NFT mint address of the position to copy"
  91. size="large"
  92. />
  93. </Form.Item>
  94. <Form.Item
  95. label="Maximum Investment (USD)"
  96. name="maxUsdValue"
  97. rules={[
  98. { required: true, message: 'Please enter maximum USD value' },
  99. {
  100. type: 'number',
  101. min: 0.01,
  102. message: 'Must be greater than 0.01',
  103. },
  104. ]}
  105. >
  106. <InputNumber
  107. placeholder="e.g., 5, 10, 50"
  108. min={0.01}
  109. step={1}
  110. style={{ width: '100%' }}
  111. size="large"
  112. formatter={(value) =>
  113. `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
  114. }
  115. parser={(value) => {
  116. const parsed = value?.replace(/\$\s?|(,*)/g, '') || '0'
  117. return parseFloat(parsed) || 0
  118. }}
  119. />
  120. </Form.Item>
  121. <Form.Item>
  122. <Button
  123. type="primary"
  124. htmlType="submit"
  125. size="large"
  126. icon={loading ? <LoadingOutlined /> : <CopyOutlined />}
  127. loading={loading}
  128. block
  129. >
  130. {loading ? 'Copying Position...' : 'Copy Position'}
  131. </Button>
  132. </Form.Item>
  133. </Form>
  134. </Card>
  135. {positionInfo && (
  136. <Card style={{ marginTop: '24px' }} title="Position Details">
  137. <Descriptions column={1} bordered>
  138. <Descriptions.Item label="Pool Address">
  139. <Text code>{positionInfo.poolAddress}</Text>
  140. </Descriptions.Item>
  141. <Descriptions.Item label="Tick Range">
  142. {positionInfo.tickLower} - {positionInfo.tickUpper}
  143. </Descriptions.Item>
  144. <Descriptions.Item label="Price Range">
  145. {positionInfo.priceLower} - {positionInfo.priceUpper}
  146. </Descriptions.Item>
  147. <Descriptions.Item label="Base Token">
  148. {positionInfo.base}
  149. </Descriptions.Item>
  150. <Descriptions.Item label="Base Amount">
  151. {positionInfo.baseAmount}
  152. </Descriptions.Item>
  153. <Descriptions.Item label="Other Amount Max">
  154. {positionInfo.otherAmountMax}
  155. </Descriptions.Item>
  156. <Descriptions.Item label="Estimated Value">
  157. ${positionInfo.estimatedValue.toFixed(2)} USD
  158. </Descriptions.Item>
  159. </Descriptions>
  160. </Card>
  161. )}
  162. <Card style={{ marginTop: '24px' }}>
  163. <Title level={4}>How it works:</Title>
  164. <Space orientation="vertical" size="small">
  165. <Text>
  166. 1. Enter the NFT mint address of the position you want to copy
  167. </Text>
  168. <Text>2. Set your maximum investment amount in USD</Text>
  169. <Text>3. The system will automatically:</Text>
  170. <Text style={{ marginLeft: '16px' }}>
  171. • Fetch the position details (price range, pool info)
  172. </Text>
  173. <Text style={{ marginLeft: '16px' }}>
  174. • Calculate optimal token amounts within your budget
  175. </Text>
  176. <Text style={{ marginLeft: '16px' }}>
  177. • Create a new position with the same price range
  178. </Text>
  179. <Text style={{ marginLeft: '16px' }}>
  180. • Add a memo linking to the original position
  181. </Text>
  182. </Space>
  183. </Card>
  184. </div>
  185. )
  186. }