@usdctofiat/offramp 3.0.2 → 3.1.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/CHANGELOG.md +21 -0
- package/README.md +129 -24
- package/dist/{chunk-F7DVUBMT.js → chunk-XQ7HOLLB.js} +390 -445
- package/dist/chunk-XQ7HOLLB.js.map +1 -0
- package/dist/errors-DzTiflBh.d.cts +409 -0
- package/dist/errors-DzTiflBh.d.ts +409 -0
- package/dist/index.cjs +408 -441
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -5
- package/dist/index.d.ts +9 -5
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +481 -460
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +23 -13
- package/dist/react.d.ts +23 -13
- package/dist/react.js +106 -27
- package/dist/react.js.map +1 -1
- package/package.json +5 -3
- package/dist/chunk-F7DVUBMT.js.map +0 -1
- package/dist/errors-DKQ2N_ih.d.cts +0 -217
- package/dist/errors-DKQ2N_ih.d.ts +0 -217
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Replace the PayPal/Wise Peer extension recovery path with the headless
|
|
13
|
+
`authenticate` metadata bridge after upstream removed sidepanel proof flows in
|
|
14
|
+
`zkp2p-clients@3cfa50430b9c6a2a57c3c1c1100756702feded77`, including the
|
|
15
|
+
identity-attestation and maker-registration metadata step required before
|
|
16
|
+
retrying deposit creation.
|
|
17
|
+
- Reject shortened or non-PayPal URLs in the PayPal.me username normalizer so
|
|
18
|
+
inputs like `t.co/...` cannot be converted into a fake PayPal username.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Bump `@zkp2p/sdk` to `0.4.1`.
|
|
23
|
+
|
|
24
|
+
## [3.0.3] - 2026-05-20
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Return canonical OTC share links from `getOtcLink()` and `enableOtc()` using
|
|
29
|
+
`https://otc.usdctofiat.xyz/d/<escrow>/<depositId>`.
|
|
30
|
+
|
|
10
31
|
## [3.0.2] - 2026-05-19
|
|
11
32
|
|
|
12
33
|
### Fixed
|
package/README.md
CHANGED
|
@@ -37,6 +37,16 @@ const result = await offramp(walletClient, {
|
|
|
37
37
|
// { depositId: "362", txHash: "0x...", resumed: false }
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
## Fees
|
|
41
|
+
|
|
42
|
+
Every deposit created with this SDK is delegated to the **Delegate vault**'s rate manager (the SDK always calls `setRateManager` to the vault — see [How It Works](#how-it-works)). That rate manager charges a **0.10% (10 bps) manager fee**:
|
|
43
|
+
|
|
44
|
+
- **Who pays:** the buyer (taker). The fee is deducted from the USDC released to them at fill time — it is **not** added to your deposit cost and is **not** baked into the displayed rate.
|
|
45
|
+
- **Where it goes:** the Delegate vault (Galleon Labs).
|
|
46
|
+
- **Discover it in code:** `createOfframp({ walletClient }).getVaultStatus().feeRateBps` returns the current `feeRateBps` (`10`).
|
|
47
|
+
|
|
48
|
+
The only other attribution the SDK attaches is the on-chain referrer tag `referrer: "galleonlabs"` (ERC-8021 builder attribution). That is **not a fee** — no separate referral/`referrerFee` is configured by this SDK. There is no SDK spread either: `getQuote()` returns `vaultSpreadBps: 0`.
|
|
49
|
+
|
|
40
50
|
## Deposit Management
|
|
41
51
|
|
|
42
52
|
```typescript
|
|
@@ -102,7 +112,7 @@ const { depositId, otcLink } = await offramp(walletClient, {
|
|
|
102
112
|
identifier: "alice",
|
|
103
113
|
otcTaker: "0xBuyerAddress",
|
|
104
114
|
});
|
|
105
|
-
// otcLink = "https://usdctofiat.xyz/
|
|
115
|
+
// otcLink = "https://otc.usdctofiat.xyz/d/0x.../362"
|
|
106
116
|
// Share otcLink with the buyer — they open it, connect their wallet, and fill the order.
|
|
107
117
|
```
|
|
108
118
|
|
|
@@ -118,7 +128,24 @@ getOtcLink("362"); // just the URL, no tx
|
|
|
118
128
|
|
|
119
129
|
## Resumable
|
|
120
130
|
|
|
121
|
-
`offramp()` is idempotent
|
|
131
|
+
`offramp()` is idempotent on the **delegation step**. If an undelegated deposit exists for the wallet, it skips straight to delegation. Handles browser crashes, failed delegation, and retries automatically. Just call `offramp()` again.
|
|
132
|
+
|
|
133
|
+
## Idempotency & server-side dedup
|
|
134
|
+
|
|
135
|
+
`OfframpParams.idempotencyKey` is **browser-only**: the replay cache is backed by `sessionStorage`, so it is a **no-op in Node and workers** — a backend integrator who passes a key gets zero duplicate protection there. It is also honored only by the `Offramp` class (`createDeposit`) and the `useOfframp` hook; the standalone `offramp()` function ignores it entirely. When you pass a key in a runtime with no `sessionStorage`, the SDK emits a one-time `console.warn` so you find out at startup instead of via duplicate deposits.
|
|
136
|
+
|
|
137
|
+
For server-side dedup, check for an existing open deposit before creating a new one (this is the same idea as the [resume behavior](#resumable) above):
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import { createOfframp, deposits } from "@usdctofiat/offramp";
|
|
141
|
+
|
|
142
|
+
const existing = await deposits(walletAddress);
|
|
143
|
+
const open = existing.find((d) => d.status === "active" && d.remainingUsdc > 0);
|
|
144
|
+
|
|
145
|
+
const result = open
|
|
146
|
+
? open // reuse it — don't create a duplicate
|
|
147
|
+
: await createOfframp({ walletClient }).createDeposit(params);
|
|
148
|
+
```
|
|
122
149
|
|
|
123
150
|
## Error Handling
|
|
124
151
|
|
|
@@ -144,7 +171,8 @@ try {
|
|
|
144
171
|
PayPal and Wise makers must register their handle inside the Peer (PeerAuth)
|
|
145
172
|
browser extension before the first deposit. The SDK throws `OfframpError`
|
|
146
173
|
with code `EXTENSION_REGISTRATION_REQUIRED` when curator rejects a maker for
|
|
147
|
-
this reason. Drive
|
|
174
|
+
this reason. Drive install, connection approval, headless seller-credential capture,
|
|
175
|
+
seller-credential upload, and retry via the React hook:
|
|
148
176
|
|
|
149
177
|
```tsx
|
|
150
178
|
import { PLATFORMS, CURRENCIES, OFFRAMP_ERROR_CODES } from "@usdctofiat/offramp";
|
|
@@ -153,23 +181,18 @@ import { useOfframp, usePeerExtensionRegistration } from "@usdctofiat/offramp/re
|
|
|
153
181
|
function PayPalSellButton({ walletClient }) {
|
|
154
182
|
const { offramp, lastError } = useOfframp();
|
|
155
183
|
const peer = usePeerExtensionRegistration(PLATFORMS.PAYPAL);
|
|
184
|
+
const depositParams = {
|
|
185
|
+
amount: "100",
|
|
186
|
+
platform: PLATFORMS.PAYPAL,
|
|
187
|
+
currency: CURRENCIES.USD,
|
|
188
|
+
identifier: "alicepay", // PayPal.me username, NOT email
|
|
189
|
+
};
|
|
156
190
|
|
|
157
191
|
const needsExtension = lastError?.code === OFFRAMP_ERROR_CODES.EXTENSION_REGISTRATION_REQUIRED;
|
|
158
192
|
|
|
159
193
|
return (
|
|
160
194
|
<>
|
|
161
|
-
<button
|
|
162
|
-
onClick={() =>
|
|
163
|
-
offramp(walletClient, {
|
|
164
|
-
amount: "100",
|
|
165
|
-
platform: PLATFORMS.PAYPAL,
|
|
166
|
-
currency: CURRENCIES.USD,
|
|
167
|
-
identifier: "alicepay", // PayPal.me username, NOT email
|
|
168
|
-
})
|
|
169
|
-
}
|
|
170
|
-
>
|
|
171
|
-
Sell USDC
|
|
172
|
-
</button>
|
|
195
|
+
<button onClick={() => offramp(walletClient, depositParams)}>Sell USDC</button>
|
|
173
196
|
|
|
174
197
|
{needsExtension && (
|
|
175
198
|
<div>
|
|
@@ -183,7 +206,14 @@ function PayPalSellButton({ walletClient }) {
|
|
|
183
206
|
</button>
|
|
184
207
|
)}
|
|
185
208
|
{peer.phase === "ready" && (
|
|
186
|
-
<button onClick={peer.
|
|
209
|
+
<button onClick={peer.startRegistrationCapture} disabled={peer.busy}>
|
|
210
|
+
{peer.info?.ctaLabel ?? "Register with Peer"}
|
|
211
|
+
</button>
|
|
212
|
+
)}
|
|
213
|
+
{peer.capturedMetadata?.sarCredentialCapture?.credentialBundle && !peer.error && (
|
|
214
|
+
<button onClick={() => peer.completeRegistration(walletClient, depositParams)}>
|
|
215
|
+
Continue registration
|
|
216
|
+
</button>
|
|
187
217
|
)}
|
|
188
218
|
{peer.info?.ctaSubtext && <small>{peer.info.ctaSubtext}</small>}
|
|
189
219
|
</div>
|
|
@@ -193,31 +223,106 @@ function PayPalSellButton({ walletClient }) {
|
|
|
193
223
|
}
|
|
194
224
|
```
|
|
195
225
|
|
|
196
|
-
Or drive the handshake manually via `peerExtensionSdk
|
|
226
|
+
Or drive the handshake manually via `peerExtensionSdk`.
|
|
227
|
+
`getPeerExtensionRegistrationAuthParams(...)` returns
|
|
228
|
+
`actionType: transfer_<provider>` and `captureMode: "sellerCredential"`:
|
|
197
229
|
|
|
198
230
|
```typescript
|
|
199
|
-
import {
|
|
231
|
+
import {
|
|
232
|
+
CURRENCIES,
|
|
233
|
+
PLATFORMS,
|
|
234
|
+
completePeerExtensionRegistration,
|
|
235
|
+
getPeerExtensionRegistrationAuthParams,
|
|
236
|
+
isPeerExtensionMetadataBridgeAvailable,
|
|
237
|
+
offramp,
|
|
238
|
+
peerExtensionSdk,
|
|
239
|
+
} from "@usdctofiat/offramp";
|
|
240
|
+
|
|
241
|
+
const params = getPeerExtensionRegistrationAuthParams("paypal");
|
|
242
|
+
if (!params) throw new Error("PayPal registration is not extension-gated.");
|
|
243
|
+
// Auth params use captureMode: "sellerCredential" and actionType: "transfer_paypal".
|
|
244
|
+
const depositParams = {
|
|
245
|
+
amount: "100",
|
|
246
|
+
platform: PLATFORMS.PAYPAL,
|
|
247
|
+
currency: CURRENCIES.USD,
|
|
248
|
+
identifier: "alicepay",
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const startCapture = () => {
|
|
252
|
+
const unsubscribe = peerExtensionSdk.onMetadataMessage(async (message) => {
|
|
253
|
+
if (message.platform !== params.platform) return;
|
|
254
|
+
if (!message.sarCredentialCapture?.credentialBundle) return;
|
|
255
|
+
await completePeerExtensionRegistration({
|
|
256
|
+
platform: PLATFORMS.PAYPAL,
|
|
257
|
+
identifier: "alicepay",
|
|
258
|
+
capturedMetadata: message,
|
|
259
|
+
});
|
|
260
|
+
await offramp(walletClient, depositParams);
|
|
261
|
+
});
|
|
262
|
+
peerExtensionSdk.authenticate(params);
|
|
263
|
+
return unsubscribe;
|
|
264
|
+
};
|
|
200
265
|
|
|
201
266
|
const state = await peerExtensionSdk.getState();
|
|
202
|
-
if (state === "needs_install") {
|
|
267
|
+
if (state === "needs_install" || !isPeerExtensionMetadataBridgeAvailable()) {
|
|
203
268
|
peerExtensionSdk.openInstallPage();
|
|
204
269
|
} else if (state === "needs_connection") {
|
|
205
270
|
const approved = await peerExtensionSdk.requestConnection();
|
|
206
|
-
if (approved)
|
|
271
|
+
if (approved) startCapture();
|
|
207
272
|
} else {
|
|
208
|
-
|
|
273
|
+
startCapture();
|
|
209
274
|
}
|
|
210
275
|
```
|
|
211
276
|
|
|
212
|
-
|
|
277
|
+
The captured metadata passed to `completePeerExtensionRegistration(...)` must
|
|
278
|
+
include `sarCredentialCapture.credentialBundle`. Completion validates the
|
|
279
|
+
captured `offchainId` against your identifier and validates the payee hash in
|
|
280
|
+
the credential bundle. PayPal first registers the normalized handle through
|
|
281
|
+
`/v2/makers/create` to resolve `hashedOnchainId`; Wise uses
|
|
282
|
+
`credentialBundle.payeeIdHash` directly. Both paths upload the seller credential
|
|
283
|
+
bundle to `/v2/makers/<processor>/<payeeHash>/seller-credential` and return
|
|
284
|
+
`sellerCredentialResponse` plus `sellerCredentialStatus`. After that succeeds,
|
|
285
|
+
call `offramp()` again with the same handle.
|
|
213
286
|
|
|
214
287
|
## How It Works
|
|
215
288
|
|
|
216
|
-
Every deposit is automatically delegated to the Delegate vault for oracle-based rate management, auto-closes when filled, and is attributed to Galleon Labs via ERC-8021.
|
|
289
|
+
Every deposit is automatically delegated to the Delegate vault for oracle-based rate management (which charges a 0.10% manager fee — see [Fees](#fees)), auto-closes when filled, and is attributed to Galleon Labs via ERC-8021.
|
|
290
|
+
|
|
291
|
+
## Testing your integration
|
|
292
|
+
|
|
293
|
+
There is no public staging or sandbox deployment — the SDK targets Base mainnet and the production curator API. Two ways to test:
|
|
294
|
+
|
|
295
|
+
- **Small-amount mainnet runs (recommended).** Create a 1 USDC deposit against a real payment handle, confirm it on [basescan](https://basescan.org) and via `deposits(walletAddress)`, then `close()` it. This exercises the real approve → register → deposit → delegate path end to end.
|
|
296
|
+
- **Proxy or mock the curator REST surface.** Pass `apiBaseUrl` to point the SDK's maker registration/validation calls (`/v2/makers/*`) at a local proxy or mock. Defaults are byte-for-byte unchanged when omitted. Only the curator REST surface is overridable — the indexer and Base RPC always target mainnet, and on-chain calls still need a real wallet client.
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { createOfframp, PLATFORMS, CURRENCIES } from "@usdctofiat/offramp";
|
|
300
|
+
|
|
301
|
+
const sdk = createOfframp({
|
|
302
|
+
walletClient,
|
|
303
|
+
apiBaseUrl: "http://localhost:8787", // proxy/mock of https://api.zkp2p.xyz
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await sdk.createDeposit({
|
|
307
|
+
amount: "1",
|
|
308
|
+
platform: PLATFORMS.REVOLUT,
|
|
309
|
+
currency: CURRENCIES.EUR,
|
|
310
|
+
identifier: "alice",
|
|
311
|
+
});
|
|
312
|
+
```
|
|
217
313
|
|
|
218
314
|
## Webhooks
|
|
219
315
|
|
|
220
|
-
Subscribe to live `deposit.created`, `deposit.filled`, `deposit.partially_filled`, `deposit.closed`, and `otc.taken` events for your attributed deposits.
|
|
316
|
+
Subscribe to live `deposit.created`, `deposit.filled`, `deposit.partially_filled`, `deposit.closed`, and `otc.taken` events for your attributed deposits. Register endpoints at [usdctofiat.xyz/developers](https://usdctofiat.xyz/developers) with your Peerlytics API key. HMAC-SHA256 signed deliveries use the `X-Usdctofiat-Signature: t=<unix>,v1=<hex>` header. Reference HMAC receiver in the [starters repo](https://github.com/ADWilkinson/usdctofiat-peerlytics-starters). (`otc.enabled` / `otc.disabled` are intentionally not emitted.)
|
|
317
|
+
|
|
318
|
+
There are **two separate webhook families** — different event vocabularies, different dispatchers, different registration surfaces. Pick the one for the product you integrate:
|
|
319
|
+
|
|
320
|
+
| Family | Events | Emitted by | Register at |
|
|
321
|
+
| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------- | -------------------------------------------------------------- |
|
|
322
|
+
| **Offramp** (this SDK) | `deposit.created`, `deposit.filled`, `deposit.partially_filled`, `deposit.closed`, `otc.taken` | usdctofiat offramp dispatcher | [usdctofiat.xyz/developers](https://usdctofiat.xyz/developers) |
|
|
323
|
+
| **Integrator** ([`@peerlytics/sdk`](https://www.npmjs.com/package/@peerlytics/sdk)) | `deposit.created`, `intent.signaled`, `intent.fulfilled`, `deposit.rate_updated` | Peerlytics cron dispatcher | [peerlytics.xyz/developers](https://peerlytics.xyz/developers) |
|
|
324
|
+
|
|
325
|
+
One Peerlytics API key authenticates both. The integrator family also accepts the legacy aliases `intent.created`, `intent.filled`, `rate.updated` on registration (normalized server-side).
|
|
221
326
|
|
|
222
327
|
## Links
|
|
223
328
|
|