@orbs-network/spot-react 1.1.40 → 1.1.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +413 -257
- package/dist/spot-react/package.json.d.ts +1 -1
- package/dist/spot-react/src/hooks/order-hooks.d.ts.map +1 -1
- package/dist/spot-react/src/hooks/use-cancel-order.d.ts +2 -0
- package/dist/spot-react/src/hooks/use-cancel-order.d.ts.map +1 -1
- package/dist/spot-react/src/hooks/use-form-data.d.ts +10 -31
- package/dist/spot-react/src/hooks/use-form-data.d.ts.map +1 -1
- package/dist/spot-react/src/hooks/use-history-order.d.ts.map +1 -1
- package/dist/spot-react/src/hooks/use-repermit-data.d.ts +7 -0
- package/dist/spot-react/src/hooks/use-repermit-data.d.ts.map +1 -0
- package/dist/spot-react/src/hooks/use-repermit-order-data.d.ts +14 -31
- package/dist/spot-react/src/hooks/use-repermit-order-data.d.ts.map +1 -1
- package/dist/spot-react/src/hooks/use-submit-order-panel.d.ts +3 -1
- package/dist/spot-react/src/hooks/use-submit-order-panel.d.ts.map +1 -1
- package/dist/spot-react/src/hooks/use-submit-order.d.ts.map +1 -1
- package/dist/spot-react/src/index.d.ts +2 -1
- package/dist/spot-react/src/index.d.ts.map +1 -1
- package/dist/spot-react/src/spot-context.d.ts.map +1 -1
- package/dist/spot-react/src/types.d.ts +1 -2
- package/dist/spot-react/src/types.d.ts.map +1 -1
- package/dist/spot-react.js +2461 -2508
- package/dist/spot-react.umd.cjs +7 -7
- package/dist/spot-ui/src/lib/analytics.d.ts +7 -5
- package/dist/spot-ui/src/lib/analytics.d.ts.map +1 -1
- package/dist/spot-ui/src/lib/build-repermit-order-data.d.ts +21 -41
- package/dist/spot-ui/src/lib/build-repermit-order-data.d.ts.map +1 -1
- package/dist/spot-ui/src/lib/consts.d.ts +1 -0
- package/dist/spot-ui/src/lib/consts.d.ts.map +1 -1
- package/dist/spot-ui/src/lib/lib.d.ts +15 -2
- package/dist/spot-ui/src/lib/lib.d.ts.map +1 -1
- package/dist/spot-ui/src/lib/orders/index.d.ts +6 -3
- package/dist/spot-ui/src/lib/orders/index.d.ts.map +1 -1
- package/dist/spot-ui/src/lib/types.d.ts +18 -17
- package/dist/spot-ui/src/lib/types.d.ts.map +1 -1
- package/dist/spot-ui/src/lib/utils.d.ts +1 -1
- package/dist/spot-ui/src/lib/utils.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/spot-react/src/hooks/use-addresses.d.ts +0 -4
- package/dist/spot-react/src/hooks/use-addresses.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -2,383 +2,539 @@
|
|
|
2
2
|
|
|
3
3
|
React SDK for building TWAP, Limit, Stop-Loss, and Take-Profit order interfaces on top of the [Orbs Spot protocol](https://www.orbs.com/).
|
|
4
4
|
|
|
5
|
+
`spot-react` owns Spot order state and exposes it through `SpotProvider` and `useSpot()`. The host DEX remains responsible for swap-form state, wallet access, components, styling, translations, routing, and modal shells.
|
|
6
|
+
|
|
7
|
+
For the complete integration workflow, see the [Spot React integration skill](https://github.com/orbs-network/spot-ui/tree/master/skills/spot-react-integration) and the [reference implementation](https://github.com/orbs-network/spot-ui/blob/master/apps/web/components/spot/spot-form.tsx).
|
|
8
|
+
|
|
9
|
+
## Before You Start
|
|
10
|
+
|
|
11
|
+
Every DEX needs a member of the exported `Partners` enum and a server-side Orbs Spot configuration for its supported chains. The SDK fetches contract and adapter addresses from that configuration. If the DEX is not configured yet, contact [@dTWAPSupportGroup](https://t.me/dTWAPSupportGroup) before integrating.
|
|
12
|
+
|
|
13
|
+
### RePermit configuration security
|
|
14
|
+
|
|
15
|
+
The `/config` endpoint is a security boundary. The SDK trusts successful JSON responses without client-side schema or contract-identity validation. It uses `domain.verifyingContract` as the ERC-20 approval spender and the v2 cancellation contract, and uses the returned adapter, reactor, and executor when constructing orders. Deployments must use the trusted Orbs endpoint over TLS, and contract-address changes require explicit approval from the protocol/security owner.
|
|
16
|
+
|
|
17
|
+
`SpotProvider` starts a shared configuration query as soon as the partner and chain are known. Concurrent consumers reuse the same query, successful responses remain cached for that partner/chain/environment, and failed requests are retried twice. `useRePermitData()` exposes the query state and `refetch`; the standard submit panel exposes the same recovery action as `useSpot().submitOrderButton.retry`.
|
|
18
|
+
|
|
19
|
+
The completed integration should:
|
|
20
|
+
|
|
21
|
+
- keep tokens, typed amount, balances, USD prices, and quote state in the DEX's existing swap store or context;
|
|
22
|
+
- provide all five `walletInteractions` methods using the DEX's existing wallet stack;
|
|
23
|
+
- render TWAP, Limit, Stop-Loss, and Take-Profit entry points using DEX-native navigation and components;
|
|
24
|
+
- use `@orbs-network/swap-ui` for the order review/progress flow inside the DEX's modal shell;
|
|
25
|
+
- render order history and cancellation inside `SpotProvider` scope or through a context-preserving portal.
|
|
26
|
+
|
|
5
27
|
## Installation
|
|
6
28
|
|
|
29
|
+
Install the latest Spot packages with the host application's package manager. Do not mix lockfiles.
|
|
30
|
+
|
|
7
31
|
```bash
|
|
8
|
-
npm install @orbs-network/spot-react
|
|
32
|
+
npm install @orbs-network/spot-react@latest @orbs-network/swap-ui@latest
|
|
33
|
+
# or: pnpm add @orbs-network/spot-react@latest @orbs-network/swap-ui@latest
|
|
34
|
+
# or: yarn add @orbs-network/spot-react@latest @orbs-network/swap-ui@latest
|
|
9
35
|
```
|
|
10
36
|
|
|
37
|
+
If migrating from `@orbs-network/twap-ui`, remove it before installing the packages above.
|
|
38
|
+
|
|
11
39
|
### Peer Dependencies
|
|
12
40
|
|
|
13
|
-
|
|
41
|
+
The host application must also provide:
|
|
14
42
|
|
|
15
43
|
```bash
|
|
16
44
|
npm install @tanstack/react-query bignumber.js react-error-boundary zustand react react-dom
|
|
17
45
|
```
|
|
18
46
|
|
|
19
|
-
| Package
|
|
20
|
-
|
|
|
21
|
-
| `@tanstack/react-query
|
|
22
|
-
| `bignumber.js`
|
|
23
|
-
| `react-error-boundary` | `^6.0.0`
|
|
24
|
-
| `zustand`
|
|
25
|
-
| `react`
|
|
26
|
-
| `react-dom`
|
|
47
|
+
| Package | Version |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `@tanstack/react-query` | `^5.90.12` |
|
|
50
|
+
| `bignumber.js` | `^9.3.1` |
|
|
51
|
+
| `react-error-boundary` | `^6.0.0` |
|
|
52
|
+
| `zustand` | `^5.0.9` |
|
|
53
|
+
| `react` | `^18 \|\| ^19` |
|
|
54
|
+
| `react-dom` | `^18 \|\| ^19` |
|
|
55
|
+
|
|
56
|
+
`viem` is not a dependency. Adapt the wallet library already used by the DEX through `walletInteractions`.
|
|
57
|
+
|
|
58
|
+
## Integration Model
|
|
27
59
|
|
|
28
|
-
|
|
60
|
+
Keep the DEX swap form as the source of truth. Pass the following adapted values directly to `SpotProvider`:
|
|
61
|
+
|
|
62
|
+
| Value | Expected shape |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| Selected tokens | `Token` objects with `address`, `symbol`, `decimals`, and `logoUrl` |
|
|
65
|
+
| Typed source amount | User-facing decimal string, for example `"1.25"` |
|
|
66
|
+
| Quote output | Raw destination-token amount for the current typed input |
|
|
67
|
+
| Balances | Raw integer strings |
|
|
68
|
+
| USD prices | USD value of exactly one whole source/destination token |
|
|
69
|
+
| Chain and account | Values from the connected wallet/account state |
|
|
70
|
+
|
|
71
|
+
If several Spot components need the same DEX-owned values, expose a small DEX adapter context. Do not copy the swap state into a parallel Spot store, prop-drill long value lists, or create a hook whose only purpose is forwarding props to `SpotProvider`.
|
|
72
|
+
|
|
73
|
+
Memoize objects with `useMemo` and functions with `useCallback`. In particular, keep stable identities for tokens, `marketReferencePrice`, `walletInteractions`, and `callbacks`.
|
|
74
|
+
|
|
75
|
+
## Provider Setup
|
|
29
76
|
|
|
30
77
|
```tsx
|
|
78
|
+
import { useMemo } from "react";
|
|
31
79
|
import {
|
|
32
|
-
SpotProvider,
|
|
33
|
-
useSpot,
|
|
34
80
|
Module,
|
|
35
81
|
Partners,
|
|
82
|
+
SpotProvider,
|
|
83
|
+
type Callbacks,
|
|
84
|
+
type MarketReferencePrice,
|
|
85
|
+
type Token,
|
|
36
86
|
type WalletInteractions,
|
|
37
87
|
} from "@orbs-network/spot-react";
|
|
38
88
|
|
|
39
|
-
function
|
|
89
|
+
function SpotOrderForm({ module }: { module: Module }) {
|
|
90
|
+
// Read these from the DEX's existing swap and wallet state.
|
|
91
|
+
const {
|
|
92
|
+
account,
|
|
93
|
+
chainId,
|
|
94
|
+
inputCurrency,
|
|
95
|
+
outputCurrency,
|
|
96
|
+
inputBalance,
|
|
97
|
+
outputBalance,
|
|
98
|
+
typedInputAmount,
|
|
99
|
+
quotedInputAmount,
|
|
100
|
+
quoteOutputRaw,
|
|
101
|
+
isQuoteLoading,
|
|
102
|
+
inputUsdPrice,
|
|
103
|
+
outputUsdPrice,
|
|
104
|
+
refetchBalances,
|
|
105
|
+
dexWallet,
|
|
106
|
+
} = useDexSpotAdapter();
|
|
107
|
+
|
|
108
|
+
const srcToken = useMemo<Token | undefined>(() => {
|
|
109
|
+
if (!inputCurrency) return undefined;
|
|
110
|
+
return {
|
|
111
|
+
address: inputCurrency.address,
|
|
112
|
+
symbol: inputCurrency.symbol,
|
|
113
|
+
decimals: inputCurrency.decimals,
|
|
114
|
+
logoUrl: inputCurrency.logoUrl,
|
|
115
|
+
};
|
|
116
|
+
}, [inputCurrency]);
|
|
117
|
+
|
|
118
|
+
const dstToken = useMemo<Token | undefined>(() => {
|
|
119
|
+
if (!outputCurrency) return undefined;
|
|
120
|
+
return {
|
|
121
|
+
address: outputCurrency.address,
|
|
122
|
+
symbol: outputCurrency.symbol,
|
|
123
|
+
decimals: outputCurrency.decimals,
|
|
124
|
+
logoUrl: outputCurrency.logoUrl,
|
|
125
|
+
};
|
|
126
|
+
}, [outputCurrency]);
|
|
127
|
+
|
|
128
|
+
const marketReferencePrice = useMemo<MarketReferencePrice>(() => {
|
|
129
|
+
const shouldQuote = Boolean(
|
|
130
|
+
typedInputAmount && inputCurrency && outputCurrency,
|
|
131
|
+
);
|
|
132
|
+
const isStale = shouldQuote && typedInputAmount !== quotedInputAmount;
|
|
133
|
+
const value = !shouldQuote || isStale ? undefined : quoteOutputRaw;
|
|
134
|
+
const isLoading = shouldQuote && (isStale || isQuoteLoading);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
value,
|
|
138
|
+
isLoading,
|
|
139
|
+
noLiquidity: shouldQuote && !isLoading && !value,
|
|
140
|
+
};
|
|
141
|
+
}, [
|
|
142
|
+
inputCurrency,
|
|
143
|
+
isQuoteLoading,
|
|
144
|
+
outputCurrency,
|
|
145
|
+
quoteOutputRaw,
|
|
146
|
+
quotedInputAmount,
|
|
147
|
+
typedInputAmount,
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
const walletInteractions = useMemo<WalletInteractions>(
|
|
151
|
+
() => createWalletInteractions(dexWallet),
|
|
152
|
+
[dexWallet],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const callbacks = useMemo<Callbacks>(
|
|
156
|
+
() => ({
|
|
157
|
+
onWrapSuccess: () => refetchBalances(),
|
|
158
|
+
onOrderCreated: () => refetchBalances(),
|
|
159
|
+
onOrderFilled: () => refetchBalances(),
|
|
160
|
+
onOrdersProgressUpdate: () => refetchBalances(),
|
|
161
|
+
onCancelOrderSuccess: () => refetchBalances(),
|
|
162
|
+
}),
|
|
163
|
+
[refetchBalances],
|
|
164
|
+
);
|
|
165
|
+
|
|
40
166
|
return (
|
|
41
167
|
<SpotProvider
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
srcToken={srcToken}
|
|
168
|
+
partner={Partners.Quick}
|
|
169
|
+
module={module}
|
|
170
|
+
typedInputAmount={typedInputAmount}
|
|
171
|
+
priceProtection={3}
|
|
172
|
+
minChunkSizeUsd={5}
|
|
173
|
+
marketReferencePrice={marketReferencePrice}
|
|
174
|
+
walletInteractions={walletInteractions}
|
|
175
|
+
chainId={chainId}
|
|
176
|
+
account={account}
|
|
177
|
+
srcToken={srcToken}
|
|
52
178
|
dstToken={dstToken}
|
|
53
|
-
srcBalance={
|
|
54
|
-
dstBalance={
|
|
55
|
-
srcUsd1Token={
|
|
56
|
-
dstUsd1Token={
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
fees={0.25}
|
|
179
|
+
srcBalance={inputBalance?.toString()}
|
|
180
|
+
dstBalance={outputBalance?.toString()}
|
|
181
|
+
srcUsd1Token={inputUsdPrice}
|
|
182
|
+
dstUsd1Token={outputUsdPrice}
|
|
183
|
+
callbacks={callbacks}
|
|
184
|
+
appId="my-dex"
|
|
185
|
+
fees={0.25}
|
|
60
186
|
>
|
|
61
|
-
<
|
|
187
|
+
<SpotFormContent />
|
|
62
188
|
</SpotProvider>
|
|
63
189
|
);
|
|
64
190
|
}
|
|
65
191
|
```
|
|
66
192
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
The host app provides wallet interaction handlers via the `walletInteractions` prop. This keeps spot-react wallet-agnostic — it works with viem, ethers, web3.js, or any wallet library.
|
|
70
|
-
|
|
71
|
-
```tsx
|
|
72
|
-
const walletInteractions: WalletInteractions = {
|
|
73
|
-
// Wrap native token (e.g. ETH -> WETH). Wait for receipt, return tx hash.
|
|
74
|
-
wrapNativeToken: async (amountWei) => { ... },
|
|
75
|
-
|
|
76
|
-
// Approve ERC-20 token spending. Wait for receipt, return tx hash.
|
|
77
|
-
approveToken: async ({ token, amount, spender }) => { ... },
|
|
193
|
+
`marketReferencePrice.value` is the DEX quote's raw destination amount for the current `typedInputAmount`, not a standalone per-token price. If the quote belongs to an older input or token pair, omit `value` and report `isLoading: true` until a current quote arrives.
|
|
78
194
|
|
|
79
|
-
|
|
80
|
-
cancelOrder: async ({ order, contractAddress, args }) => { ... },
|
|
195
|
+
Use the connected wallet chain as the UI source of truth. The provider falls back internally to a supported partner chain for configuration lookups when `chainId` is absent or unsupported, so the DEX submit area must still show connect-wallet or switch-network controls and block submission.
|
|
81
196
|
|
|
82
|
-
|
|
83
|
-
signOrder: async ({ domain, types, primaryType, message, account }) => { ... },
|
|
197
|
+
### SpotProvider Props
|
|
84
198
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
199
|
+
| Prop | Type | Required | Description |
|
|
200
|
+
| --- | --- | --- | --- |
|
|
201
|
+
| `partner` | `Partners` | Yes | DEX partner enum |
|
|
202
|
+
| `module` | `Module` | Yes | `TWAP`, `LIMIT`, `STOP_LOSS`, or `TAKE_PROFIT` |
|
|
203
|
+
| `typedInputAmount` | `string` | Yes | User-facing source amount from DEX state |
|
|
204
|
+
| `priceProtection` | `number` | Yes | Price Protection percentage; this is not swap slippage |
|
|
205
|
+
| `minChunkSizeUsd` | `number` | Yes | Minimum trade chunk size in USD |
|
|
206
|
+
| `marketReferencePrice` | `MarketReferencePrice` | Yes | `{ value?, isLoading?, noLiquidity? }` for the current DEX quote |
|
|
207
|
+
| `walletInteractions` | `WalletInteractions` | Yes | Five wallet methods implemented by the DEX |
|
|
208
|
+
| `chainId` | `number` | No | Connected wallet chain ID |
|
|
209
|
+
| `account` | `string` | No | Connected wallet address |
|
|
210
|
+
| `appId` | `string` | No | Analytics app ID |
|
|
211
|
+
| `srcToken` | `Token` | No | Source token metadata |
|
|
212
|
+
| `dstToken` | `Token` | No | Destination token metadata |
|
|
213
|
+
| `srcBalance` | `string` | No | Raw source-token balance |
|
|
214
|
+
| `dstBalance` | `string` | No | Raw destination-token balance |
|
|
215
|
+
| `srcUsd1Token` | `string` | No | USD value of one whole source token |
|
|
216
|
+
| `dstUsd1Token` | `string` | No | USD value of one whole destination token |
|
|
217
|
+
| `enableQueryParams` | `boolean` | No | Sync supported form state to URL query parameters |
|
|
218
|
+
| `callbacks` | `Callbacks` | No | Lifecycle and field-change callbacks |
|
|
219
|
+
| `fees` | `number` | No | Fee percentage, for example `0.25` |
|
|
220
|
+
| `isDev` | `boolean` | No | Use development services/configuration |
|
|
221
|
+
| `supportLegacyOrders` | `boolean` | No | Include supported legacy v1 orders in history |
|
|
222
|
+
|
|
223
|
+
Although balances and USD prices are optional in the TypeScript type, production integrations should pass them so validation, loading states, minimum trade size, and review details are correct.
|
|
89
224
|
|
|
90
|
-
|
|
225
|
+
## WalletInteractions
|
|
91
226
|
|
|
92
|
-
|
|
227
|
+
The DEX must implement all five methods. Write methods must wait for the transaction receipt, throw when the transaction reverts, and then return the transaction hash.
|
|
93
228
|
|
|
94
229
|
```tsx
|
|
95
|
-
import { erc20Abi } from "viem";
|
|
96
|
-
|
|
97
230
|
const walletInteractions: WalletInteractions = {
|
|
98
231
|
wrapNativeToken: async (amountWei) => {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
address: wTokenAddress,
|
|
103
|
-
args: [],
|
|
104
|
-
value: BigInt(amountWei),
|
|
105
|
-
chain: walletClient.chain,
|
|
106
|
-
account: walletClient.account!,
|
|
107
|
-
});
|
|
108
|
-
await publicClient.waitForTransactionReceipt({ hash, confirmations: 2 });
|
|
109
|
-
return hash;
|
|
232
|
+
const txHash = await dexWallet.wrapNativeToken(amountWei);
|
|
233
|
+
await dexWallet.waitForReceipt(txHash);
|
|
234
|
+
return txHash;
|
|
110
235
|
},
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
args: [spender as `0x${string}`, maxUint256],
|
|
118
|
-
chain: walletClient.chain,
|
|
119
|
-
account: walletClient.account!,
|
|
236
|
+
|
|
237
|
+
approveToken: async ({ tokenAddress, amount, spenderAddress }) => {
|
|
238
|
+
const txHash = await dexWallet.approveToken({
|
|
239
|
+
tokenAddress,
|
|
240
|
+
amount,
|
|
241
|
+
spenderAddress,
|
|
120
242
|
});
|
|
121
|
-
await
|
|
122
|
-
return
|
|
243
|
+
await dexWallet.waitForReceipt(txHash);
|
|
244
|
+
return txHash;
|
|
123
245
|
},
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
246
|
+
|
|
247
|
+
cancelOrder: async ({ order, contractAddress, args, abi }) => {
|
|
248
|
+
const txHash = await dexWallet.writeContract({
|
|
249
|
+
address: contractAddress,
|
|
250
|
+
abi,
|
|
127
251
|
functionName: "cancel",
|
|
128
|
-
|
|
129
|
-
args: args as any,
|
|
130
|
-
chain: walletClient.chain,
|
|
131
|
-
account: walletClient.account!,
|
|
252
|
+
args,
|
|
132
253
|
});
|
|
133
|
-
await
|
|
134
|
-
return
|
|
135
|
-
},
|
|
136
|
-
signOrder: async ({ domain, types, primaryType, message, account }) => {
|
|
137
|
-
return walletClient.signTypedData({ domain, types, primaryType, message, account });
|
|
138
|
-
},
|
|
139
|
-
getAllowance: async ({ tokenAddress, spenderAddress }) => {
|
|
140
|
-
const result = await publicClient.readContract({
|
|
141
|
-
address: tokenAddress as `0x${string}`,
|
|
142
|
-
abi: erc20Abi,
|
|
143
|
-
functionName: "allowance",
|
|
144
|
-
args: [walletClient.account!.address, spenderAddress as `0x${string}`],
|
|
145
|
-
});
|
|
146
|
-
return String(result);
|
|
254
|
+
await dexWallet.waitForReceipt(txHash);
|
|
255
|
+
return txHash;
|
|
147
256
|
},
|
|
257
|
+
|
|
258
|
+
signOrder: ({ domain, types, primaryType, message, account }) =>
|
|
259
|
+
dexWallet.signTypedData({ domain, types, primaryType, message, account }),
|
|
260
|
+
|
|
261
|
+
getAllowance: ({ tokenAddress, spenderAddress }) =>
|
|
262
|
+
dexWallet.getAllowance({ tokenAddress, spenderAddress }),
|
|
148
263
|
};
|
|
149
264
|
```
|
|
150
265
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
| `
|
|
156
|
-
| `
|
|
157
|
-
| `
|
|
158
|
-
| `priceProtection` | `number` | Yes | Slippage tolerance percentage |
|
|
159
|
-
| `minChunkSizeUsd` | `number` | Yes | Minimum trade chunk size in USD |
|
|
160
|
-
| `marketReferencePrice` | `MarketReferencePrice` | Yes | `{ value?: string, isLoading?: boolean, noLiquidity?: boolean }` |
|
|
161
|
-
| `walletInteractions` | `WalletInteractions` | Yes | Wallet interaction handlers |
|
|
162
|
-
| `chainId` | `number` | No | Connected chain ID |
|
|
163
|
-
| `account` | `string` | No | Connected wallet address |
|
|
164
|
-
| `appId` | `string` | No | Analytics app ID |
|
|
165
|
-
| `srcToken` | `Token` | No | `{ address, symbol, decimals, logoUrl }` |
|
|
166
|
-
| `dstToken` | `Token` | No | `{ address, symbol, decimals, logoUrl }` |
|
|
167
|
-
| `srcBalance` | `string` | No | Source balance in wei |
|
|
168
|
-
| `dstBalance` | `string` | No | Destination balance in wei |
|
|
169
|
-
| `srcUsd1Token` | `string` | No | USD price of 1 source token |
|
|
170
|
-
| `dstUsd1Token` | `string` | No | USD price of 1 destination token |
|
|
171
|
-
| `callbacks` | `Callbacks` | No | Lifecycle event handlers |
|
|
172
|
-
| `fees` | `number` | No | Fee percentage (e.g. 0.25) |
|
|
173
|
-
| `isDev` | `boolean` | No | Enable dev mode |
|
|
174
|
-
| `overrides` | `Overrides` | No | Initial state overrides |
|
|
266
|
+
| Method | Contract |
|
|
267
|
+
| --- | --- |
|
|
268
|
+
| `wrapNativeToken(amountWei)` | Wrap native currency, wait for confirmation, return tx hash |
|
|
269
|
+
| `approveToken({ tokenAddress, amount, spenderAddress })` | Approve the requested spender, wait for confirmation, return tx hash |
|
|
270
|
+
| `cancelOrder({ order, contractAddress, args, abi })` | Call `cancel` with the supplied ABI and args, wait for confirmation, return tx hash |
|
|
271
|
+
| `signOrder({ domain, types, primaryType, message, account })` | Sign the supplied EIP-712 data and return signature hex |
|
|
272
|
+
| `getAllowance({ tokenAddress, spenderAddress })` | Return the connected account's raw allowance as a string |
|
|
175
273
|
|
|
176
|
-
## useSpot()
|
|
274
|
+
## Building the Form with useSpot()
|
|
177
275
|
|
|
178
|
-
|
|
276
|
+
Every component rendered under `SpotProvider` can call `useSpot()`:
|
|
179
277
|
|
|
180
278
|
```tsx
|
|
181
279
|
const spot = useSpot();
|
|
182
280
|
```
|
|
183
281
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
| Panel | Description |
|
|
282
|
+
| Value | Key returns |
|
|
187
283
|
| --- | --- |
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
190
|
-
| `
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
195
|
-
| `
|
|
196
|
-
| `
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
199
|
-
| `
|
|
200
|
-
| `
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
203
|
-
|
|
204
|
-
|
|
284
|
+
| `dstTokenPanel` | `value`, `valueWei`, `isLoading`, `usd` |
|
|
285
|
+
| `tradesAmountPanel` | `totalTrades`, `maxTrades`, `onChange`, `error`, per-trade amounts, source/destination tokens |
|
|
286
|
+
| `durationPanel` | `duration`, input/unit callbacks, `milliseconds`, `error` |
|
|
287
|
+
| `fillDelayPanel` | `fillDelay`, input/unit callbacks, `milliseconds`, `error` |
|
|
288
|
+
| `limitPricePanel` | Raw/UI price, percentage, toggle, input/reset callbacks, tokens, USD, loading/error state |
|
|
289
|
+
| `triggerPricePanel` | Raw/UI trigger price, percentage, input/reset callbacks, tokens, per-chunk amounts, USD, error state |
|
|
290
|
+
| `pricePanel` | Inversion state/callback, source/destination tokens, market-price state |
|
|
291
|
+
| `disclaimerPanel` | Disclaimer translation key or `undefined` |
|
|
292
|
+
| `inputError` | `{ type, args }` or `undefined` |
|
|
293
|
+
| `submitOrderButton` | `disabled`, `loading`, configuration `error`, and `retry` |
|
|
294
|
+
| `orderExecutionPanel` | Submission, status, steps, errors, resets, resolved tokens, and tx hashes |
|
|
295
|
+
| `orderHistoryPanel` | Filtered order lists, loading state, and `refetchOrders` |
|
|
296
|
+
| `derivedFormData` | Review amounts, prices, timing, fees, order type, spender, and RePermit data |
|
|
297
|
+
| `supportedChains` | Supported chain IDs for the selected partner |
|
|
298
|
+
| `module` | Current `Module` |
|
|
299
|
+
| `refetchUntilStatusSynced` | Mutation used to reconcile cancellation status |
|
|
300
|
+
|
|
301
|
+
Child components should call `useSpot()` themselves instead of receiving hook-returned panels through intermediate props.
|
|
302
|
+
|
|
303
|
+
When `submitOrderButton.error` is set, render a translated retry label and call `submitOrderButton.retry()` instead of opening the review modal. Keep the button disabled while `loading` is true. Legacy v1 order history does not depend on RePermit configuration; v2 history, approvals, v2 cancellation, and submission resume after a successful retry.
|
|
304
|
+
|
|
305
|
+
### Panel Visibility
|
|
306
|
+
|
|
307
|
+
| Panel | TWAP | LIMIT | STOP_LOSS | TAKE_PROFIT |
|
|
308
|
+
| --- | --- | --- | --- | --- |
|
|
309
|
+
| Trades amount | Yes | — | — | — |
|
|
310
|
+
| Fill delay | Yes | — | — | — |
|
|
311
|
+
| Duration | — | Yes | Yes | Yes |
|
|
312
|
+
| Limit price | Yes, optional | Always on | Optional | Optional |
|
|
313
|
+
| Trigger price | — | — | Yes | Yes |
|
|
314
|
+
|
|
315
|
+
Use `TimeUnit.Minutes`, `TimeUnit.Hours`, and `TimeUnit.Days` for duration and fill-delay controls. Resolve error and disclaimer keys through the DEX's i18n system.
|
|
316
|
+
|
|
317
|
+
### Display Amounts
|
|
318
|
+
|
|
319
|
+
Raw fields such as `dstTokenPanel.valueWei`, `tradesAmountPanel.amountPerTrade`, `derivedFormData.feesAmount`, and history fill amounts are integer strings. Convert them into the DEX's native amount type before display when possible:
|
|
205
320
|
|
|
206
321
|
```tsx
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
parsedError, // { code, message } on failure
|
|
214
|
-
onSwapSuccess, // Call when user acknowledges success
|
|
215
|
-
confirmButtonLoading, // Loading state for confirm button
|
|
216
|
-
srcToken, // Resolved source token
|
|
217
|
-
dstToken, // Resolved destination token
|
|
218
|
-
wrapTxHash, // Wrap transaction hash
|
|
219
|
-
approveTxHash, // Approve transaction hash
|
|
220
|
-
} = useSpot().orderExecutionPanel;
|
|
322
|
+
const amount = CurrencyAmount.fromRawAmount(
|
|
323
|
+
inputCurrency,
|
|
324
|
+
spot.tradesAmountPanel.amountPerTrade,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
return `${amount.toSignificant()} ${inputCurrency.symbol}`;
|
|
221
328
|
```
|
|
222
329
|
|
|
223
|
-
|
|
330
|
+
Use `*UI` fields for editable text inputs or when the DEX has no amount object. Do not format raw integer strings directly for user display.
|
|
331
|
+
|
|
332
|
+
## Submit and Progress Modal
|
|
333
|
+
|
|
334
|
+
Use `useSpot().orderExecutionPanel` for execution state, `derivedFormData` for review details, and `@orbs-network/swap-ui`'s `SwapFlow` for the creation/progress UI.
|
|
224
335
|
|
|
225
336
|
```tsx
|
|
226
|
-
import {
|
|
337
|
+
import { SwapFlow } from "@orbs-network/swap-ui";
|
|
338
|
+
import { useSpot } from "@orbs-network/spot-react";
|
|
339
|
+
|
|
340
|
+
function SpotOrderFlow() {
|
|
341
|
+
const {
|
|
342
|
+
status,
|
|
343
|
+
parsedError,
|
|
344
|
+
srcToken,
|
|
345
|
+
dstToken,
|
|
346
|
+
stepIndex,
|
|
347
|
+
totalSteps,
|
|
348
|
+
} = useSpot().orderExecutionPanel;
|
|
349
|
+
const form = useSpot().derivedFormData;
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<SwapFlow
|
|
353
|
+
inAmount={form.srcAmountUI}
|
|
354
|
+
outAmount={form.dstAmountUI}
|
|
355
|
+
inToken={{ symbol: srcToken?.symbol, logoUrl: srcToken?.logoUrl }}
|
|
356
|
+
outToken={{ symbol: dstToken?.symbol, logoUrl: dstToken?.logoUrl }}
|
|
357
|
+
swapStatus={status}
|
|
358
|
+
currentStepIndex={stepIndex}
|
|
359
|
+
totalSteps={totalSteps}
|
|
360
|
+
components={{
|
|
361
|
+
Main: <SwapFlow.Main inUsd={form.srcAmountUsd} outUsd={form.dstAmountUsd} />,
|
|
362
|
+
Success: <SwapFlow.Success title="Order created" />,
|
|
363
|
+
Failed: <SwapFlow.Failed error={parsedError?.message} />,
|
|
364
|
+
}}
|
|
365
|
+
/>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
```
|
|
227
369
|
|
|
228
|
-
|
|
229
|
-
const { cancelOrder, isLoading, isSuccess, isError, error, txHash } = useCancelOrder(order);
|
|
370
|
+
Wrap `SwapFlow` in the DEX's modal shell and skin it with DEX colors, surfaces, typography, token logos, and loaders. Once `orderExecutionPanel.status` is set, hide the review details, confirm button, duplicate title, and secondary footer actions so the progress/success/failure content owns the modal.
|
|
230
371
|
|
|
231
|
-
|
|
232
|
-
await cancelOrder();
|
|
372
|
+
### Execution State and Reset
|
|
233
373
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
374
|
+
`orderExecutionPanel` includes:
|
|
375
|
+
|
|
376
|
+
- `onSubmit`, `status`, `isLoading`, `isSuccess`, `isFailed`, and `confirmButtonLoading`;
|
|
377
|
+
- `step`, `stepIndex`, `totalSteps`, and `pendingSteps`;
|
|
378
|
+
- `parsedError`, `error`, `srcToken`, `dstToken`, `wrapTxHash`, and `approveTxHash`;
|
|
379
|
+
- `resetCurrentSwap()` and `resetState()`.
|
|
380
|
+
|
|
381
|
+
`spot-react` does not clear the DEX input. Do that only when a successful modal closes:
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
const { status, isSuccess, resetCurrentSwap, resetState } =
|
|
385
|
+
useSpot().orderExecutionPanel;
|
|
386
|
+
|
|
387
|
+
const onClose = useCallback(() => {
|
|
388
|
+
setIsModalOpen(false);
|
|
389
|
+
|
|
390
|
+
if (isSuccess) {
|
|
391
|
+
setInputAmount("");
|
|
392
|
+
setTimeout(resetState, 500);
|
|
393
|
+
} else if (status) {
|
|
394
|
+
setTimeout(resetCurrentSwap, 500);
|
|
395
|
+
}
|
|
396
|
+
}, [isSuccess, resetCurrentSwap, resetState, setInputAmount, status]);
|
|
237
397
|
```
|
|
238
398
|
|
|
399
|
+
Failed or rejected submissions should keep the user's input. The short delay lets the close animation finish before state resets.
|
|
400
|
+
|
|
239
401
|
## Callbacks
|
|
240
402
|
|
|
241
|
-
|
|
403
|
+
Callbacks cover wallet notifications, analytics, field synchronization, and balance refresh:
|
|
242
404
|
|
|
243
405
|
```tsx
|
|
244
406
|
const callbacks: Callbacks = {
|
|
245
|
-
// Token wrapping
|
|
246
407
|
onWrapRequest: () => {},
|
|
247
408
|
onWrapSuccess: ({ txHash, explorerUrl, amount }) => {},
|
|
248
|
-
|
|
249
|
-
// Token approval
|
|
250
409
|
onApproveRequest: () => {},
|
|
251
410
|
onApproveSuccess: ({ txHash, explorerUrl, token, amount }) => {},
|
|
252
|
-
|
|
253
|
-
// Order lifecycle
|
|
254
411
|
onSignOrderRequest: () => {},
|
|
255
412
|
onSignOrderSuccess: (signature) => {},
|
|
256
413
|
onSignOrderError: (error) => {},
|
|
257
414
|
onOrderCreated: (order) => {},
|
|
258
415
|
onOrderFilled: (order) => {},
|
|
259
|
-
|
|
260
|
-
// Errors
|
|
416
|
+
onOrdersProgressUpdate: (orders) => {},
|
|
261
417
|
onSubmitOrderFailed: ({ code, message }) => {},
|
|
262
418
|
onSubmitOrderRejected: () => {},
|
|
263
|
-
|
|
264
|
-
// Order cancellation
|
|
265
419
|
onCancelOrderRequest: (order) => {},
|
|
266
420
|
onCancelOrderSuccess: ({ order, txHash, explorerUrl }) => {},
|
|
267
421
|
onCancelOrderFailed: (error) => {},
|
|
268
|
-
|
|
269
|
-
// Progress (good place to refetch balances)
|
|
270
|
-
onOrdersProgressUpdate: (orders) => {},
|
|
271
|
-
|
|
272
|
-
// Clipboard
|
|
273
422
|
onCopy: () => {},
|
|
274
|
-
|
|
275
|
-
// State change callbacks
|
|
276
423
|
onLimitPriceChange: (price) => {},
|
|
424
|
+
onLimitPricePercentChange: (percent) => {},
|
|
277
425
|
onTriggerPriceChange: (price) => {},
|
|
426
|
+
onTriggerPricePercentChange: (percent) => {},
|
|
278
427
|
onDurationChange: (duration) => {},
|
|
279
428
|
onFillDelayChange: (fillDelay) => {},
|
|
280
429
|
onChunksChange: (chunks) => {},
|
|
281
430
|
};
|
|
282
431
|
```
|
|
283
432
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
The SDK returns string keys for user-facing messages. You should resolve these through your own i18n system.
|
|
433
|
+
Refetch balances in `onWrapSuccess`, `onOrderCreated`, `onOrderFilled`, `onOrdersProgressUpdate`, and `onCancelOrderSuccess`. Avoid an `onOrderCreated` toast unless the host DEX specifically wants one; the submit modal already shows creation success.
|
|
287
434
|
|
|
288
|
-
|
|
435
|
+
## Cancellation and Order History
|
|
289
436
|
|
|
290
|
-
|
|
291
|
-
- `"limitOrderDisclaimer"` — shown for limit and TWAP-limit orders
|
|
292
|
-
- `"marketOrderDisclaimer"` — shown for market orders
|
|
293
|
-
- `"triggerMarketPriceDisclaimer"` — shown for stop-loss at market price
|
|
437
|
+
Use the exported cancellation hook inside `SpotProvider`:
|
|
294
438
|
|
|
295
|
-
|
|
439
|
+
```tsx
|
|
440
|
+
import { OrderStatus, useCancelOrder } from "@orbs-network/spot-react";
|
|
296
441
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
- `"minChunksError"` (args: `{ minChunks }`)
|
|
301
|
-
- `"minTradeSizeError"` (args: `{ minTradeSize }`)
|
|
302
|
-
- `"minDurationError"` / `"maxDurationError"` (args: `{ duration }`)
|
|
303
|
-
- `"minFillDelayError"` / `"maxFillDelayError"` (args: `{ fillDelay }`)
|
|
304
|
-
- `"noLiquidity"`
|
|
442
|
+
function CancelButton({ order }) {
|
|
443
|
+
const { cancelOrder, disabled, isLoading, isSuccess, isError, error, txHash } =
|
|
444
|
+
useCancelOrder(order);
|
|
305
445
|
|
|
306
|
-
|
|
446
|
+
if (order.status !== OrderStatus.Open) return null;
|
|
307
447
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
448
|
+
return (
|
|
449
|
+
<button onClick={cancelOrder} disabled={disabled || isLoading}>
|
|
450
|
+
{isLoading ? "Cancelling..." : "Cancel"}
|
|
451
|
+
</button>
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
```
|
|
314
455
|
|
|
315
|
-
|
|
456
|
+
For v2 orders, `disabled` remains true while RePermit configuration is loading or unavailable. Legacy v1 history and cancellation do not require that configuration. V2 history is added after configuration succeeds because its request needs the returned exchange adapter.
|
|
316
457
|
|
|
317
|
-
|
|
318
|
-
| --- | --- | --- | --- | --- |
|
|
319
|
-
| Trades amount | Yes | — | — | — |
|
|
320
|
-
| Fill delay | Yes | — | — | — |
|
|
321
|
-
| Duration | — | Yes | Yes | Yes |
|
|
322
|
-
| Limit price | Yes | Yes | Yes | Yes |
|
|
323
|
-
| Trigger price | — | — | Yes | Yes |
|
|
324
|
-
|
|
325
|
-
## Helper Hooks
|
|
458
|
+
Order history is available from `useSpot().orderHistoryPanel`:
|
|
326
459
|
|
|
327
460
|
```tsx
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
} from "@orbs-network/spot-react";
|
|
461
|
+
const { orders, isLoading, isRefetching, refetchOrders } =
|
|
462
|
+
useSpot().orderHistoryPanel;
|
|
463
|
+
|
|
464
|
+
// orders.all, orders.open, orders.completed, orders.cancelled, orders.expired
|
|
333
465
|
```
|
|
334
466
|
|
|
335
|
-
|
|
467
|
+
Use `useDerivedHistoryOrder(order, srcToken?, dstToken?)` for display fields. For large histories, use the virtualization library already present in the DEX for both the orders and fills lists. Store the selected order ID, then look up the current order from `orders.all`; do not store a stale copy of the order object.
|
|
336
468
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
getConfig, // (partner, chainId) => SpotConfig
|
|
345
|
-
} from "@orbs-network/spot-react";
|
|
346
|
-
```
|
|
469
|
+
Keep history, details, and modal portals under `SpotProvider` context. Details should include execution summary, order info, fills, explorer/copy actions, and cancellation for open orders.
|
|
470
|
+
|
|
471
|
+
## Translations
|
|
472
|
+
|
|
473
|
+
The SDK returns keys rather than final user-facing strings.
|
|
474
|
+
|
|
475
|
+
Disclaimer keys are:
|
|
347
476
|
|
|
348
|
-
|
|
477
|
+
- `limitOrderDisclaimer`
|
|
478
|
+
- `marketOrderDisclaimer`
|
|
479
|
+
- `triggerMarketPriceDisclaimer`
|
|
349
480
|
|
|
350
|
-
|
|
481
|
+
Input errors have the shape `{ type, args }`. Resolve `type` through the DEX's i18n system and interpolate `args`, including values such as `maxChunks`, `minChunks`, `minTradeSize`, `duration`, and `fillDelay`. Current duration and fill-delay arguments are human-readable; custom or older integrations should convert raw milliseconds before display.
|
|
482
|
+
|
|
483
|
+
## Helper and Advanced APIs
|
|
351
484
|
|
|
352
485
|
```tsx
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
},
|
|
363
|
-
}}
|
|
364
|
-
// ...other props
|
|
365
|
-
/>
|
|
486
|
+
import {
|
|
487
|
+
useAmountUi, // (decimals?, rawAmount?) => formatted amount
|
|
488
|
+
useExplorerLink, // (txHash?) => explorer URL
|
|
489
|
+
useNetwork, // () => current network metadata
|
|
490
|
+
useRePermitData, // () => { data, error, isLoading, refetch }
|
|
491
|
+
useSignOrder, // low-level signing hook
|
|
492
|
+
useSubmitOrder, // low-level submission mutation
|
|
493
|
+
useSwapExecution, // low-level execution state
|
|
494
|
+
} from "@orbs-network/spot-react";
|
|
366
495
|
```
|
|
367
496
|
|
|
368
|
-
|
|
497
|
+
Normal integrations should submit through `useSpot().orderExecutionPanel`. Use the low-level hooks only when deliberately replacing the built-in execution flow. `useRePermitData()` must be called under `SpotProvider`; most integrations should use the configuration state already exposed by `useSpot().submitOrderButton`.
|
|
498
|
+
|
|
499
|
+
Public utilities and constants include:
|
|
369
500
|
|
|
370
501
|
```tsx
|
|
371
502
|
import {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
503
|
+
getMinChunkSizeUsd,
|
|
504
|
+
getNetwork,
|
|
505
|
+
getOrderExecutionRate,
|
|
506
|
+
getOrderFillDelayMillis,
|
|
507
|
+
getOrderLimitPriceRate,
|
|
508
|
+
getPartnerChains,
|
|
509
|
+
getPartners,
|
|
510
|
+
getTriggerPriceRate,
|
|
511
|
+
eqIgnoreCase,
|
|
512
|
+
isNativeAddress,
|
|
513
|
+
DISCLAIMER_URL,
|
|
514
|
+
ORBS_TWAP_FAQ_URL,
|
|
515
|
+
ORBS_SLTP_FAQ_URL,
|
|
516
|
+
ORBS_LOGO,
|
|
517
|
+
ORBS_WEBSITE_URL,
|
|
518
|
+
SPOT_VERSION,
|
|
519
|
+
networks,
|
|
379
520
|
} from "@orbs-network/spot-react";
|
|
380
521
|
```
|
|
381
522
|
|
|
523
|
+
Always import from `@orbs-network/spot-react`. Do not import from `dist/*` or package-internal source paths, and verify public exports when upgrading.
|
|
524
|
+
|
|
525
|
+
## Integration Checklist
|
|
526
|
+
|
|
527
|
+
- Use the DEX's existing tokens, inputs, selectors, components, styling, wallet controls, and routing.
|
|
528
|
+
- Keep chain switching out of the Spot token selector; use the DEX network control.
|
|
529
|
+
- Keep the form visible while disconnected; replace only the submit area with connect/switch controls.
|
|
530
|
+
- Persist Price Protection separately from swap slippage and hide the DEX slippage setting while Spot is active.
|
|
531
|
+
- Use `@orbs-network/swap-ui` for order creation/progress content.
|
|
532
|
+
- Convert raw amounts into the DEX's amount type before display.
|
|
533
|
+
- Use callbacks for balance refetch and reset the input only after success.
|
|
534
|
+
- Put Spot tabs beside the Swap tab using the DEX's navigation pattern.
|
|
535
|
+
- Split production integrations into focused `components`, `hooks`, `context`, and `utils` files.
|
|
536
|
+
- Do not add another error boundary around Spot; `SpotProvider` already includes one.
|
|
537
|
+
|
|
382
538
|
## License
|
|
383
539
|
|
|
384
540
|
MIT
|