@shapeshiftoss/swap-widget 0.7.0 → 0.9.0
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 +172 -2
- package/dist/index.css +19 -0
- package/dist/index.d.ts +16 -2
- package/dist/index.js +428 -209
- package/package.json +11 -6
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ An embeddable React widget that enables multi-chain token swaps using ShapeShift
|
|
|
11
11
|
- [Wallet Connection](#wallet-connection)
|
|
12
12
|
- [Props Reference](#props-reference)
|
|
13
13
|
- [Filtering Chains and Assets](#filtering-chains-and-assets)
|
|
14
|
+
- [Exact Output and Locked Destinations](#exact-output-and-locked-destinations)
|
|
14
15
|
- [Theming](#theming)
|
|
15
16
|
- [Examples](#examples)
|
|
16
17
|
- [Exported Types](#exported-types)
|
|
@@ -39,7 +40,7 @@ npm install react react-dom \
|
|
|
39
40
|
@tanstack/react-query \
|
|
40
41
|
@reown/appkit @reown/appkit-adapter-wagmi \
|
|
41
42
|
@reown/appkit-adapter-bitcoin @reown/appkit-adapter-solana \
|
|
42
|
-
@solana/wallet-adapter-
|
|
43
|
+
@solana/wallet-adapter-phantom @solana/wallet-adapter-solflare @solana/web3.js
|
|
43
44
|
```
|
|
44
45
|
|
|
45
46
|
- **React 18 or 19** is supported (`^18.0.0 || ^19.0.0`).
|
|
@@ -139,6 +140,10 @@ when `allowShapeshiftRedirect` is enabled.
|
|
|
139
140
|
| `allowedSwapperNames` | `SwapperName[]` | all enabled | Limit quotes to specific swappers. See [Supported Swappers](#supported-swappers). |
|
|
140
141
|
| `allowShapeshiftRedirect`| `boolean` | `true` | When a swap isn't executable in-widget, redirect to app.shapeshift.com instead of hiding it. |
|
|
141
142
|
| `isBuyAssetLocked` | `boolean` | `false` | Prevent the user from changing the buy asset. |
|
|
143
|
+
| `defaultBuyAmountCryptoBaseUnit` | `string` | – | Drive the trade from the buy side: the user receives exactly this amount and the sell amount is derived. Restricts routing to exact-output swappers. See [Exact Output and Locked Destinations](#exact-output-and-locked-destinations). |
|
|
144
|
+
| `isBuyAmountLocked` | `boolean` | `false` | Prevent the user from changing the buy amount. When an amount is supplied it also locks the buy asset, since a base-unit amount is meaningless without the asset it counts. |
|
|
145
|
+
| `defaultReceiveAddress` | `string` | – | Prefill the destination address. Falls back to the connected wallet's address for the buy chain when unset. |
|
|
146
|
+
| `isReceiveAddressLocked` | `boolean` | `false` | Prevent the user from changing the destination address. |
|
|
142
147
|
| `theme` | `ThemeMode \| ThemeConfig` | `"dark"` | Theme mode (`"light"` or `"dark"`) or a full theme configuration object. See [Theming](#theming). |
|
|
143
148
|
| `defaultSlippage` | `string` | `"0.5"` | Default slippage tolerance, as a percentage string. |
|
|
144
149
|
| `showPoweredBy` | `boolean` | `true` | Show the "Powered by ShapeShift" footer. |
|
|
@@ -183,6 +188,162 @@ function App() {
|
|
|
183
188
|
}
|
|
184
189
|
```
|
|
185
190
|
|
|
191
|
+
## Exact Output and Locked Destinations
|
|
192
|
+
|
|
193
|
+
By default the user enters what they want to spend. These props invert that, and fix where the funds
|
|
194
|
+
land — letting you configure the widget for one specific swap.
|
|
195
|
+
|
|
196
|
+
### Fixed receive amount
|
|
197
|
+
|
|
198
|
+
`defaultBuyAmountCryptoBaseUnit` drives the trade from the buy side. The user still chooses what to
|
|
199
|
+
sell, but the sell amount comes back derived from whichever route they pick.
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
<SwapWidget
|
|
203
|
+
walletConnectProjectId="..."
|
|
204
|
+
defaultBuyAsset={usdcOnBase}
|
|
205
|
+
defaultBuyAmountCryptoBaseUnit="500000" // 0.5 USDC, in base units
|
|
206
|
+
isBuyAmountLocked
|
|
207
|
+
/>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Without `isBuyAmountLocked` the amount is only a **prefill**: both fields stay editable, and
|
|
211
|
+
whichever one the user types into becomes the side that drives the trade — the other is then derived
|
|
212
|
+
from the selected route. Adding the lock fixes the buy amount, and makes the sell field read-only,
|
|
213
|
+
since typing there would clear the amount you locked.
|
|
214
|
+
|
|
215
|
+
Because a base-unit amount only means something alongside the asset it counts, `isBuyAmountLocked`
|
|
216
|
+
locks the buy asset too. Changing an unlocked buy asset keeps the entered amount and recalculates its
|
|
217
|
+
base units at the new precision, matching how the sell side already behaves.
|
|
218
|
+
|
|
219
|
+
Only swappers that can honour an exact output are routed to — currently **NEAR Intents** and
|
|
220
|
+
**Relay**. The rest report `ExactOutputNotSupported` and are left out of the rate list, so expect
|
|
221
|
+
fewer routes than a normal swap, and none at all for pairs those two don't cover.
|
|
222
|
+
|
|
223
|
+
Slippage applies to the **sell** side. The amount received is fixed; what varies is what it costs.
|
|
224
|
+
|
|
225
|
+
### Fixed destination
|
|
226
|
+
|
|
227
|
+
`defaultReceiveAddress` prefills the destination. Add `isReceiveAddressLocked` to stop the user
|
|
228
|
+
changing it — the locked address then outranks both a user entry and the connected wallet's own
|
|
229
|
+
address.
|
|
230
|
+
|
|
231
|
+
| Props | Behaviour |
|
|
232
|
+
| ---------------------------- | ---------------------------------- |
|
|
233
|
+
| `defaultReceiveAddress` only | Prefilled, user can still edit it |
|
|
234
|
+
| Both | Locked to the address you supplied |
|
|
235
|
+
|
|
236
|
+
A locked address is checked against the buy asset's chain, and if it doesn't validate there the
|
|
237
|
+
widget says so and blocks the swap rather than falling back to the connected wallet — paying the
|
|
238
|
+
user's own address is never what a locked destination meant. This is reachable whenever the buy
|
|
239
|
+
asset is left unlocked, since the user can switch to a chain the address doesn't belong to.
|
|
240
|
+
|
|
241
|
+
An **unlocked** prefill is checked the same way but fails quietly: one that doesn't match the buy
|
|
242
|
+
chain is dropped, and the connected wallet's address takes over as if you had passed nothing. Verify
|
|
243
|
+
a prefill against the chain of the buy asset you pair it with.
|
|
244
|
+
|
|
245
|
+
A lock is only accepted alongside the value it locks — `isReceiveAddressLocked` on its own is a type
|
|
246
|
+
error, as is `isBuyAmountLocked` without `defaultBuyAmountCryptoBaseUnit`. Locking with nothing to
|
|
247
|
+
lock would fall back to the connected wallet's address, which is undefined whenever the wallet
|
|
248
|
+
doesn't cover the buy asset's chain, leaving no address and no way to enter one.
|
|
249
|
+
|
|
250
|
+
If you build props dynamically, import `ReceiveAddressProps` and `BuyAmountProps` and construct each
|
|
251
|
+
pair together:
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
const receiveAddressProps: ReceiveAddressProps = address
|
|
255
|
+
? { defaultReceiveAddress: address, isReceiveAddressLocked: true }
|
|
256
|
+
: {}
|
|
257
|
+
|
|
258
|
+
<SwapWidget {...receiveAddressProps} />
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Payment mode
|
|
262
|
+
|
|
263
|
+
Locking **both** the buy amount and the address — `defaultBuyAmountCryptoBaseUnit` with
|
|
264
|
+
`isBuyAmountLocked`, plus `defaultReceiveAddress` with `isReceiveAddressLocked` — puts the widget in
|
|
265
|
+
**payment mode**: a set amount, sent to an address you supplied.
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
<SwapWidget
|
|
269
|
+
walletConnectProjectId="..."
|
|
270
|
+
defaultBuyAsset={usdcOnBase}
|
|
271
|
+
defaultBuyAmountCryptoBaseUnit="500000"
|
|
272
|
+
isBuyAmountLocked
|
|
273
|
+
defaultReceiveAddress="0x…"
|
|
274
|
+
isReceiveAddressLocked
|
|
275
|
+
onSwapSuccess={txHash => recordPayment(txHash)}
|
|
276
|
+
/>
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The success screen is then terminal — no "New Swap" button. Repeating a payment means paying twice,
|
|
280
|
+
so whether there's another swap is your call rather than the widget's; use `onSwapSuccess` to decide
|
|
281
|
+
what happens next.
|
|
282
|
+
|
|
283
|
+
Locking only **one** of the two stays repeatable, and keeps the button: a set amount sent to the
|
|
284
|
+
user's own wallet, or topping up a locked address again, are both things a user may reasonably do
|
|
285
|
+
twice.
|
|
286
|
+
`isBuyAssetLocked` never affects this — restricting swaps to a given token is just a configuration.
|
|
287
|
+
|
|
288
|
+
### Redirects are disabled by either lock
|
|
289
|
+
|
|
290
|
+
The app.shapeshift.com redirect carries neither the destination nor the buy amount, so following it
|
|
291
|
+
would drop whichever constraint you set. Locking the buy amount **or** the receive address
|
|
292
|
+
therefore disables it outright: `allowShapeshiftRedirect` has no effect, and assets on
|
|
293
|
+
non-executable chains drop out of the asset pickers rather than dead-ending. The pickers share that
|
|
294
|
+
filter, so Cosmos-SDK assets go from the **buy** side too, even though a swap into them works — only
|
|
295
|
+
the sell side needs a signature. The remaining redirect-only chains lose nothing: the widget has no
|
|
296
|
+
address validator for them, so they were never usable as a destination.
|
|
297
|
+
|
|
298
|
+
Note this is a wider condition than payment mode — locking either one is enough, because a single
|
|
299
|
+
dropped constraint can send funds somewhere you didn't intend.
|
|
300
|
+
|
|
301
|
+
### Configuration is applied at mount
|
|
302
|
+
|
|
303
|
+
Every `default*` prop is applied a single time, when the widget mounts — the same semantics as
|
|
304
|
+
`defaultValue` on an `<input>`. After that the value belongs to the user, so changing the prop on an
|
|
305
|
+
already mounted widget has no effect, and neither does resolving it asynchronously: if you fetch
|
|
306
|
+
`Asset` objects, hold off rendering until you have them.
|
|
307
|
+
|
|
308
|
+
**Locked values are the exception.** A locked value is yours rather than the user's, so it tracks its
|
|
309
|
+
prop rather than seeding once — change `defaultBuyAmountCryptoBaseUnit` alongside `isBuyAmountLocked`,
|
|
310
|
+
or `defaultReceiveAddress` alongside `isReceiveAddressLocked`, and the widget follows without a
|
|
311
|
+
remount. Both apply while the user is on the input step. Once they've asked for a quote a change may
|
|
312
|
+
not land at all — that quote carries the amount and address it was built with — so change them
|
|
313
|
+
before the user starts, or remount. `defaultBuyAsset` seeds once even when `isBuyAssetLocked`.
|
|
314
|
+
|
|
315
|
+
To change anything else, or to start a fresh swap after a payment completes, **remount**.
|
|
316
|
+
If the widget lives in a modal that unmounts its children while closed, that happens for free:
|
|
317
|
+
|
|
318
|
+
```tsx
|
|
319
|
+
{isOpen && <SwapWidget onSwapSuccess={() => setIsOpen(false)} {...config} />}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Inline — or in a modal that keeps its children mounted — bump a `key` instead:
|
|
323
|
+
|
|
324
|
+
```tsx
|
|
325
|
+
const [swapSession, setSwapSession] = useState(0)
|
|
326
|
+
|
|
327
|
+
<SwapWidget
|
|
328
|
+
key={swapSession}
|
|
329
|
+
onSwapSuccess={txHash => {
|
|
330
|
+
recordSwap(txHash)
|
|
331
|
+
setSwapSession(n => n + 1)
|
|
332
|
+
}}
|
|
333
|
+
{...config}
|
|
334
|
+
/>
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Remounting is cheap. The AppKit instance and the React Query cache are module-level singletons
|
|
338
|
+
rather than widget state, so the user stays connected and asset and balance data isn't refetched —
|
|
339
|
+
only the swap itself resets.
|
|
340
|
+
|
|
341
|
+
### Refunds
|
|
342
|
+
|
|
343
|
+
If a swap can't be completed, the provider returns the funds to the **sending** address — the wallet
|
|
344
|
+
the user swapped from — not to the receive address. A locked destination does not affect where a
|
|
345
|
+
refund goes.
|
|
346
|
+
|
|
186
347
|
## Theming
|
|
187
348
|
|
|
188
349
|
The widget supports a simple light/dark mode or a full theme configuration object.
|
|
@@ -303,8 +464,10 @@ end in a specific token.
|
|
|
303
464
|
import type {
|
|
304
465
|
Asset,
|
|
305
466
|
AssetId,
|
|
467
|
+
BuyAmountProps,
|
|
306
468
|
Chain,
|
|
307
469
|
ChainId,
|
|
470
|
+
ReceiveAddressProps,
|
|
308
471
|
SwapWidgetFilters,
|
|
309
472
|
SwapWidgetProps,
|
|
310
473
|
ThemeConfig,
|
|
@@ -522,5 +685,12 @@ revenue attribution works.
|
|
|
522
685
|
- **Balances and USD prices.** When a wallet is connected, the widget shows balances and USD prices
|
|
523
686
|
for the selected assets.
|
|
524
687
|
- **Redirects.** Assets on non-executable chains (Cosmos, Zcash, Tron, Sui, TON, NEAR, Starknet)
|
|
525
|
-
send the user to app.shapeshift.com to finish the swap, unless `allowShapeshiftRedirect={false}
|
|
688
|
+
send the user to app.shapeshift.com to finish the swap, unless `allowShapeshiftRedirect={false}`
|
|
689
|
+
or the buy amount or receive address is locked (see
|
|
690
|
+
[Redirects are disabled by either lock](#redirects-are-disabled-by-either-lock)).
|
|
691
|
+
- **Configuration is applied at mount.** `default*` props are read once; locked values keep
|
|
692
|
+
tracking their prop. Remount to change anything else, or to start a fresh swap. See
|
|
693
|
+
[Configuration is applied at mount](#configuration-is-applied-at-mount).
|
|
694
|
+
- **`onSwapSuccess` reports the sell transaction.** The hash it receives is the transaction the user
|
|
695
|
+
signed on the sell chain. On cross-chain routes the destination transfer may still be in flight.
|
|
526
696
|
- **Mobile responsive.** The widget is designed to work on mobile as well as desktop.
|
package/dist/index.css
CHANGED
|
@@ -155,6 +155,22 @@
|
|
|
155
155
|
.ssw-amount-input::placeholder {
|
|
156
156
|
color: var(--ssw-text-muted);
|
|
157
157
|
}
|
|
158
|
+
.ssw-amount-input-pending::placeholder {
|
|
159
|
+
animation: ssw-placeholder-pulse 1.4s ease-in-out infinite;
|
|
160
|
+
}
|
|
161
|
+
@keyframes ssw-placeholder-pulse {
|
|
162
|
+
0%, 100% {
|
|
163
|
+
opacity: 1;
|
|
164
|
+
}
|
|
165
|
+
50% {
|
|
166
|
+
opacity: 0.3;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
@media (prefers-reduced-motion: reduce) {
|
|
170
|
+
.ssw-amount-input-pending::placeholder {
|
|
171
|
+
animation: none;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
158
174
|
.ssw-token-btn {
|
|
159
175
|
display: flex;
|
|
160
176
|
align-items: center;
|
|
@@ -446,6 +462,9 @@
|
|
|
446
462
|
.ssw-receive-row.ssw-attention {
|
|
447
463
|
box-shadow: 0 0 16px -2px var(--ssw-accent);
|
|
448
464
|
}
|
|
465
|
+
.ssw-receive-row.ssw-receive-row-invalid {
|
|
466
|
+
border-color: var(--ssw-error);
|
|
467
|
+
}
|
|
449
468
|
.ssw-quotes {
|
|
450
469
|
padding: 0 16px 16px;
|
|
451
470
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -99,7 +99,21 @@ type SwapWidgetFilters = {
|
|
|
99
99
|
allowedAssetIds?: AssetId[];
|
|
100
100
|
disabledAssetIds?: AssetId[];
|
|
101
101
|
};
|
|
102
|
-
type
|
|
102
|
+
type ReceiveAddressProps = {
|
|
103
|
+
defaultReceiveAddress: string;
|
|
104
|
+
isReceiveAddressLocked?: boolean;
|
|
105
|
+
} | {
|
|
106
|
+
defaultReceiveAddress?: never;
|
|
107
|
+
isReceiveAddressLocked?: false;
|
|
108
|
+
};
|
|
109
|
+
type BuyAmountProps = {
|
|
110
|
+
defaultBuyAmountCryptoBaseUnit: string;
|
|
111
|
+
isBuyAmountLocked?: boolean;
|
|
112
|
+
} | {
|
|
113
|
+
defaultBuyAmountCryptoBaseUnit?: never;
|
|
114
|
+
isBuyAmountLocked?: false;
|
|
115
|
+
};
|
|
116
|
+
type SwapWidgetProps = ReceiveAddressProps & BuyAmountProps & {
|
|
103
117
|
partnerCode?: string;
|
|
104
118
|
apiBaseUrl?: string;
|
|
105
119
|
allowShapeshiftRedirect?: boolean;
|
|
@@ -192,4 +206,4 @@ declare const useChains: () => AssetQueryResult<ChainInfo[]>;
|
|
|
192
206
|
declare const useAssetsByChainId: (chainId: ChainId | undefined) => AssetQueryResult<Asset[]>;
|
|
193
207
|
declare const useAssetSearch: (query: string, chainId?: ChainId) => AssetQueryResult<Asset[]>;
|
|
194
208
|
|
|
195
|
-
export { type Asset, COSMOS_CHAIN_IDS, type Chain, EVM_CHAIN_IDS, OTHER_CHAIN_IDS, REDIRECT_ONLY_CHAIN_IDS, SwapWidget, type SwapWidgetFilters, type SwapWidgetProps, SwapperName, type ThemeConfig, type ThemeMode, type TradeQuote, type TradeRate, UTXO_CHAIN_IDS, formatAmount, getBaseAsset, getChainColor, getChainIcon, getChainName, getChainType, getEvmNetworkId, getExplorerTxLink, isEvmChainId, isWidgetExecutableChainId, isWidgetSupportedChainId, parseAmount, truncateAddress, useAssetById, useAssetSearch, useAssets, useAssetsByChainId, useChains };
|
|
209
|
+
export { type Asset, type BuyAmountProps, COSMOS_CHAIN_IDS, type Chain, EVM_CHAIN_IDS, OTHER_CHAIN_IDS, REDIRECT_ONLY_CHAIN_IDS, type ReceiveAddressProps, SwapWidget, type SwapWidgetFilters, type SwapWidgetProps, SwapperName, type ThemeConfig, type ThemeMode, type TradeQuote, type TradeRate, UTXO_CHAIN_IDS, formatAmount, getBaseAsset, getChainColor, getChainIcon, getChainName, getChainType, getEvmNetworkId, getExplorerTxLink, isEvmChainId, isWidgetExecutableChainId, isWidgetSupportedChainId, parseAmount, truncateAddress, useAssetById, useAssetSearch, useAssets, useAssetsByChainId, useChains };
|