@usdctofiat/offramp 3.0.3 → 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 CHANGED
@@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
9
9
 
10
10
  ### Fixed
11
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.
12
17
  - Reject shortened or non-PayPal URLs in the PayPal.me username normalizer so
13
18
  inputs like `t.co/...` cannot be converted into a fake PayPal username.
14
19
 
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
@@ -118,7 +128,24 @@ getOtcLink("362"); // just the URL, no tx
118
128
 
119
129
  ## Resumable
120
130
 
121
- `offramp()` is idempotent. 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.
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 the three-step handshake via the React hook:
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.openVerifySidebar}>Verify in Peer</button>
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 { peerExtensionSdk } from "@usdctofiat/offramp";
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) peerExtensionSdk.openSidebar("/verify/paypal");
271
+ if (approved) startCapture();
207
272
  } else {
208
- peerExtensionSdk.openSidebar("/verify/paypal");
273
+ startCapture();
209
274
  }
210
275
  ```
211
276
 
212
- After the user completes the flow in the extension, call `offramp()` again.
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. `otc.enabled` and `otc.disabled` are reserved event names. 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).
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