@toon-protocol/client 0.14.0 → 0.14.1

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/dist/index.d.ts CHANGED
@@ -404,13 +404,21 @@ interface SignedBalanceProof extends BalanceProofParams {
404
404
  *
405
405
  * ## FULFILL data contract
406
406
  *
407
- * For a successful single-packet (non-chunked) blob upload, the DVM provider
408
- * returns the Arweave transaction ID as a **UTF-8 string, base64-encoded** in
409
- * the ILP FULFILL `data` field (the connector validates that FULFILL data is
410
- * base64). An Arweave tx ID is a 43-character base64url string (32 raw bytes).
407
+ * The deployed connector is a payment-proxy (HTTP-in-ILP): a successful blob
408
+ * upload returns the DVM's verbatim **HTTP/1.1 response message** in the ILP
409
+ * FULFILL `data` field. For a single-packet (non-chunked) upload the body is a
410
+ * JSON object:
411
411
  *
412
- * So the decode is:
413
- * `txId = utf8(base64decode(result.data))`
412
+ * HTTP/1.1 200 OK\r\n
413
+ * content-length: 189\r\n
414
+ * \r\n
415
+ * {"accept":true,"txId":"<43-char base64url>","data":"<base64 of txId>",...}
416
+ *
417
+ * We parse the HTTP envelope, fail on a non-2xx status (or `accept:false`), and
418
+ * read the Arweave tx ID from `txId` (falling back to base64-decoding `data`).
419
+ * An Arweave tx ID is a 43-character base64url string (32 raw bytes). A legacy
420
+ * fallback still accepts a bare `base64(utf8(txId))` FULFILL (no HTTP envelope)
421
+ * so non-proxy providers do not regress. See {@link extractArweaveTxId}.
414
422
  *
415
423
  * See `packages/sdk/src/arweave/arweave-dvm-handler.ts` for the server side and
416
424
  * `packages/client/tests/e2e/docker-arweave-dvm-e2e.test.ts` for the reference
@@ -2368,6 +2376,64 @@ declare function withRetry<T>(operation: () => Promise<T>, options: RetryOptions
2368
2376
  */
2369
2377
  declare function buildStoreWriteEnvelope(event: NostrEvent): Uint8Array;
2370
2378
 
2379
+ /**
2380
+ * Shared parser for the HTTP-over-ILP response carried in an ILP **FULFILL**
2381
+ * packet's `data` field.
2382
+ *
2383
+ * The deployed connector is a payment-proxy (HTTP-in-ILP): a paid write/upload
2384
+ * is reverse-proxied to the relay/DVM origin and the origin's reply is returned
2385
+ * **verbatim as a full HTTP/1.1 response message** inside the FULFILL `data`:
2386
+ *
2387
+ * HTTP/1.1 200 OK\r\n
2388
+ * content-length: 189\r\n
2389
+ * \r\n
2390
+ * {"accept":true,"txId":"4QcRav...","data":"<base64-txid>",...}
2391
+ *
2392
+ * Callers (`ToonClient.publishEvent`, `requestBlobStorage`) previously treated
2393
+ * this `data` as opaque success bytes — `publishEvent` reported success on ANY
2394
+ * FULFILL even when the embedded HTTP status was `404 Not Found`, and
2395
+ * `requestBlobStorage` base64-decoded the WHOLE response as if it were the bare
2396
+ * Arweave tx id. This module makes the HTTP envelope first-class so both paths
2397
+ * can read the real status and body.
2398
+ *
2399
+ * The full Web-`Response` reconstruction used by the h402 fetch path lives in
2400
+ * `adapters/Http402Client.ts` (`parseHttpResponse`); this is a smaller,
2401
+ * dependency-free helper that returns the status code + raw body string, which
2402
+ * is all the publish/upload paths need.
2403
+ *
2404
+ * DEFENSIVE: not every FULFILL is HTTP-enveloped (e.g. Mill-swap raw-TOON
2405
+ * FULFILLs go through `sendSwapPacket`, not these paths). If the decoded data
2406
+ * does not begin with an `HTTP/<v>` status line, `isHttp` is `false` and the
2407
+ * caller should fall back to its prior (non-HTTP) interpretation rather than
2408
+ * fail. This keeps non-HTTP FULFILLs from regressing.
2409
+ */
2410
+ /** Result of parsing FULFILL `data` as an HTTP/1.1 response. */
2411
+ interface ParsedFulfillHttp {
2412
+ /** Whether the data looked like an HTTP/1.1 response (status line present). */
2413
+ isHttp: boolean;
2414
+ /** HTTP status code (e.g. 200, 404). Only meaningful when `isHttp` is true. */
2415
+ status: number;
2416
+ /** Reason phrase from the status line (may be empty). */
2417
+ statusText: string;
2418
+ /** Decoded response body as a UTF-8 string (empty when none). */
2419
+ body: string;
2420
+ }
2421
+ /**
2422
+ * Parse FULFILL `data` bytes as an HTTP/1.1 response.
2423
+ *
2424
+ * Returns `{ isHttp: false, ... }` (without throwing) when the payload does not
2425
+ * start with an `HTTP/<v>` status line, so callers can fall back to their
2426
+ * legacy non-HTTP interpretation. When it IS an HTTP response, the status code
2427
+ * and body are extracted; a present-but-malformed status line yields
2428
+ * `isHttp: false` as well (treated as non-HTTP rather than throwing).
2429
+ */
2430
+ declare function parseFulfillHttpBytes(bytes: Uint8Array): ParsedFulfillHttp;
2431
+ /**
2432
+ * Convenience wrapper: decode a base64 FULFILL `data` string (the shape carried
2433
+ * on `IlpSendResult.data`) and parse it as an HTTP/1.1 response.
2434
+ */
2435
+ declare function parseFulfillHttp(base64Data: string): ParsedFulfillHttp;
2436
+
2371
2437
  /**
2372
2438
  * Settlement info produced by buildSettlementInfo().
2373
2439
  * Extends the core SettlementConfig shape with ilpAddress for client use.
@@ -3270,4 +3336,4 @@ declare function loadKeystore(path: string, password: string): string;
3270
3336
  */
3271
3337
  declare function writeKeystoreFile(path: string, keystore: EncryptedKeystore): void;
3272
3338
 
3273
- export { type BackupPayload, type BalanceProofParams, BtpRuntimeClient, type BtpRuntimeClientConfig, type ChainMetadata, type ChainSigner, ChannelManager, type ClaimMessage, type ClaimResolver, ConnectorError, type DiscoveredIlpPeer, type EVMClaimMessage, type EncryptedKeystore, EvmSigner, type FaucetChain, type FundWalletOptions, type FundWalletResult, type H402FetchOptions, Http402Client, type Http402ClientConfig, HttpConnectorAdmin, type HttpConnectorAdminConfig, HttpIlpClient, type HttpIlpClientConfig, type HttpIlpClientFactory, HttpRuntimeClient, type HttpRuntimeClientConfig, ILP_CLAIM_HEADER, ILP_CLAIM_WRAPPED_HEADER, ILP_PEER_ID_HEADER, type IlpTransportChoice, type InteractionResultContent, KeyManager, type KeyManagerConfig, type MinaClaimMessage, type MinaDepositReader, MinaSigner, type MinaSignerOptions, NetworkError, OnChainChannelClient, type OnChainChannelClientConfig, type ParsedX402Challenge, type PasskeyInfo, type PetDvmProvider, type PetInteractionEventData, type PetInteractionRequestParams, type PetInteractionResultData, type PetListing, type PetListingFilterOptions, type PetListingParams, type PetPurchaseRequestParams, type ProofStatus, type PublishEventResult, type RequestBlobStorageParams, type RequestBlobStorageResult, type RetryOptions, type SelectIlpTransportOptions, type SignedBalanceProof, type SolanaChannelClientOptions, type SolanaClaimMessage, SolanaSigner, type StatValues, type ToonChannelAccept, ToonClient, type ToonClientConfig, ToonClientError, type ToonIdentity, type ToonSigners, type ToonStartResult, type UnsignedNostrEvent, ValidationError, type VaultData, applyDefaults, applyNetworkPresets, buildBackupEvent, buildBackupFilter, buildPetInteractionRequest, buildPetListingEvent, buildPetPurchaseRequest, buildSettlementInfo, buildStoreWriteEnvelope, decryptMnemonic, deriveFromNsec, deriveFullIdentity, deriveNostrKeyFromMnemonic, encryptMnemonic, filterPetDvmProviders, filterPetListings, fundWallet, generateKeystore, generateMnemonic, generateRandomIdentity, getNetworkStatus, httpEndpointToBtpUrl, importKeystore, isPrfSupported, loadKeystore, parseBackupPayload, parseHttpResponse, parsePetInteractionEvent, parsePetInteractionResult, parsePetListing, parseX402Body, parseX402Challenge, proxyIlpEndpoint, readDiscoveredIlpPeer, readMinaDepositTotal, requestBlobStorage, selectIlpTransport, serializeHttpRequest, validateConfig, validateMnemonic, withRetry, writeKeystoreFile };
3339
+ export { type BackupPayload, type BalanceProofParams, BtpRuntimeClient, type BtpRuntimeClientConfig, type ChainMetadata, type ChainSigner, ChannelManager, type ClaimMessage, type ClaimResolver, ConnectorError, type DiscoveredIlpPeer, type EVMClaimMessage, type EncryptedKeystore, EvmSigner, type FaucetChain, type FundWalletOptions, type FundWalletResult, type H402FetchOptions, Http402Client, type Http402ClientConfig, HttpConnectorAdmin, type HttpConnectorAdminConfig, HttpIlpClient, type HttpIlpClientConfig, type HttpIlpClientFactory, HttpRuntimeClient, type HttpRuntimeClientConfig, ILP_CLAIM_HEADER, ILP_CLAIM_WRAPPED_HEADER, ILP_PEER_ID_HEADER, type IlpTransportChoice, type InteractionResultContent, KeyManager, type KeyManagerConfig, type MinaClaimMessage, type MinaDepositReader, MinaSigner, type MinaSignerOptions, NetworkError, OnChainChannelClient, type OnChainChannelClientConfig, type ParsedFulfillHttp, type ParsedX402Challenge, type PasskeyInfo, type PetDvmProvider, type PetInteractionEventData, type PetInteractionRequestParams, type PetInteractionResultData, type PetListing, type PetListingFilterOptions, type PetListingParams, type PetPurchaseRequestParams, type ProofStatus, type PublishEventResult, type RequestBlobStorageParams, type RequestBlobStorageResult, type RetryOptions, type SelectIlpTransportOptions, type SignedBalanceProof, type SolanaChannelClientOptions, type SolanaClaimMessage, SolanaSigner, type StatValues, type ToonChannelAccept, ToonClient, type ToonClientConfig, ToonClientError, type ToonIdentity, type ToonSigners, type ToonStartResult, type UnsignedNostrEvent, ValidationError, type VaultData, applyDefaults, applyNetworkPresets, buildBackupEvent, buildBackupFilter, buildPetInteractionRequest, buildPetListingEvent, buildPetPurchaseRequest, buildSettlementInfo, buildStoreWriteEnvelope, decryptMnemonic, deriveFromNsec, deriveFullIdentity, deriveNostrKeyFromMnemonic, encryptMnemonic, filterPetDvmProviders, filterPetListings, fundWallet, generateKeystore, generateMnemonic, generateRandomIdentity, getNetworkStatus, httpEndpointToBtpUrl, importKeystore, isPrfSupported, loadKeystore, parseBackupPayload, parseFulfillHttp, parseFulfillHttpBytes, parseHttpResponse, parsePetInteractionEvent, parsePetInteractionResult, parsePetListing, parseX402Body, parseX402Challenge, proxyIlpEndpoint, readDiscoveredIlpPeer, readMinaDepositTotal, requestBlobStorage, selectIlpTransport, serializeHttpRequest, validateConfig, validateMnemonic, withRetry, writeKeystoreFile };
package/dist/index.js CHANGED
@@ -518,6 +518,44 @@ function buildStoreWriteEnvelope(event) {
518
518
  return encodeUtf8(head + "\r\n\r\n" + body);
519
519
  }
520
520
 
521
+ // src/utils/fulfill-http.ts
522
+ var CRLF = "\r\n";
523
+ function findHeaderEnd(bytes) {
524
+ for (let i = 0; i + 3 < bytes.length; i++) {
525
+ if (bytes[i] === 13 && bytes[i + 1] === 10 && bytes[i + 2] === 13 && bytes[i + 3] === 10) {
526
+ return i + 4;
527
+ }
528
+ }
529
+ return -1;
530
+ }
531
+ function parseFulfillHttpBytes(bytes) {
532
+ const notHttp = {
533
+ isHttp: false,
534
+ status: 0,
535
+ statusText: "",
536
+ body: ""
537
+ };
538
+ const headerEnd = findHeaderEnd(bytes);
539
+ const headBytes = headerEnd === -1 ? bytes : bytes.subarray(0, headerEnd - 2);
540
+ const bodyBytes = headerEnd === -1 ? new Uint8Array(0) : bytes.subarray(headerEnd);
541
+ const headText = decodeUtf8(headBytes);
542
+ const lines = headText.split(CRLF).filter((l) => l.length > 0);
543
+ const statusLine = lines.shift();
544
+ if (!statusLine) return notHttp;
545
+ if (!statusLine.trimStart().startsWith("HTTP/")) return notHttp;
546
+ const match = /^HTTP\/\d\.\d\s+(\d{3})(?:\s+(.*))?$/.exec(statusLine.trim());
547
+ if (!match) return notHttp;
548
+ return {
549
+ isHttp: true,
550
+ status: parseInt(match[1], 10),
551
+ statusText: match[2] ?? "",
552
+ body: decodeUtf8(bodyBytes)
553
+ };
554
+ }
555
+ function parseFulfillHttp(base64Data) {
556
+ return parseFulfillHttpBytes(fromBase64(base64Data));
557
+ }
558
+
521
559
  // src/modes/http.ts
522
560
  import { BootstrapService, createDiscoveryTracker } from "@toon-protocol/core";
523
561
 
@@ -3557,15 +3595,17 @@ async function requestBlobStorage(client, secretKey, params) {
3557
3595
  return {
3558
3596
  success: false,
3559
3597
  eventId: event.id,
3560
- error: "FULFILL contained no data; expected base64-encoded Arweave tx ID"
3598
+ error: "FULFILL contained no data; expected an HTTP response with the Arweave tx ID"
3561
3599
  };
3562
3600
  }
3563
- const txId = decodeUtf8(fromBase64(result.data));
3564
- if (!ARWEAVE_TX_ID_REGEX.test(txId)) {
3601
+ let txId;
3602
+ try {
3603
+ txId = extractArweaveTxId(result.data);
3604
+ } catch (error) {
3565
3605
  return {
3566
3606
  success: false,
3567
3607
  eventId: event.id,
3568
- error: `Decoded FULFILL data is not a valid Arweave tx ID: "${txId}"`
3608
+ error: error instanceof Error ? error.message : String(error)
3569
3609
  };
3570
3610
  }
3571
3611
  return {
@@ -3574,6 +3614,49 @@ async function requestBlobStorage(client, secretKey, params) {
3574
3614
  eventId: event.id
3575
3615
  };
3576
3616
  }
3617
+ function extractArweaveTxId(base64Data) {
3618
+ const http2 = parseFulfillHttp(base64Data);
3619
+ if (!http2.isHttp) {
3620
+ const legacy = decodeUtf8(fromBase64(base64Data));
3621
+ if (!ARWEAVE_TX_ID_REGEX.test(legacy)) {
3622
+ throw new Error(
3623
+ `Decoded FULFILL data is not a valid Arweave tx ID: "${legacy}"`
3624
+ );
3625
+ }
3626
+ return legacy;
3627
+ }
3628
+ if (http2.status < 200 || http2.status >= 300) {
3629
+ const detail = http2.body ? ` - ${http2.body}` : "";
3630
+ throw new Error(
3631
+ `Blob upload failed: DVM returned HTTP ${http2.status} ${http2.statusText}`.trimEnd() + detail
3632
+ );
3633
+ }
3634
+ let parsed;
3635
+ try {
3636
+ parsed = JSON.parse(http2.body);
3637
+ } catch {
3638
+ throw new Error(
3639
+ `Blob upload response body was not valid JSON: "${http2.body}"`
3640
+ );
3641
+ }
3642
+ const body = parsed;
3643
+ if (body.accept === false) {
3644
+ const reason = typeof body.error === "string" ? `: ${body.error}` : "";
3645
+ throw new Error(`Blob upload rejected by DVM (accept:false)${reason}`);
3646
+ }
3647
+ if (typeof body.txId === "string" && ARWEAVE_TX_ID_REGEX.test(body.txId)) {
3648
+ return body.txId;
3649
+ }
3650
+ if (typeof body.data === "string" && body.data.length > 0) {
3651
+ const decoded = decodeUtf8(fromBase64(body.data));
3652
+ if (ARWEAVE_TX_ID_REGEX.test(decoded)) {
3653
+ return decoded;
3654
+ }
3655
+ }
3656
+ throw new Error(
3657
+ `Blob upload response did not contain a valid Arweave tx ID: "${http2.body}"`
3658
+ );
3659
+ }
3577
3660
 
3578
3661
  // src/adapters/Http402Client.ts
3579
3662
  var Http402Client = class {
@@ -3748,7 +3831,7 @@ function parseX402Body(body) {
3748
3831
  }
3749
3832
  return version !== void 0 ? { x402Version: version } : {};
3750
3833
  }
3751
- var CRLF = "\r\n";
3834
+ var CRLF2 = "\r\n";
3752
3835
  function concatHeadAndBody(head, body) {
3753
3836
  const headBytes = encodeUtf8(head);
3754
3837
  const out = new Uint8Array(headBytes.length + body.length);
@@ -3778,10 +3861,10 @@ function serializeHttpRequest(req) {
3778
3861
  `${req.method.toUpperCase()} ${target} HTTP/1.1`,
3779
3862
  ...headers.values()
3780
3863
  ];
3781
- const head = lines.join(CRLF) + CRLF + CRLF;
3864
+ const head = lines.join(CRLF2) + CRLF2 + CRLF2;
3782
3865
  return concatHeadAndBody(head, bodyBytes);
3783
3866
  }
3784
- function findHeaderEnd(bytes) {
3867
+ function findHeaderEnd2(bytes) {
3785
3868
  for (let i = 0; i + 3 < bytes.length; i++) {
3786
3869
  if (bytes[i] === 13 && bytes[i + 1] === 10 && bytes[i + 2] === 13 && bytes[i + 3] === 10) {
3787
3870
  return i + 4;
@@ -3790,11 +3873,11 @@ function findHeaderEnd(bytes) {
3790
3873
  return -1;
3791
3874
  }
3792
3875
  function parseHttpResponse(bytes) {
3793
- const headerEnd = findHeaderEnd(bytes);
3876
+ const headerEnd = findHeaderEnd2(bytes);
3794
3877
  const headBytes = headerEnd === -1 ? bytes : bytes.subarray(0, headerEnd - 2);
3795
3878
  const body = headerEnd === -1 ? new Uint8Array(0) : bytes.subarray(headerEnd);
3796
3879
  const headText = decodeUtf8(headBytes);
3797
- const lines = headText.split(CRLF).filter((l) => l.length > 0);
3880
+ const lines = headText.split(CRLF2).filter((l) => l.length > 0);
3798
3881
  const statusLine = lines.shift();
3799
3882
  if (!statusLine) {
3800
3883
  throw new ConnectorError(
@@ -4151,6 +4234,16 @@ var ToonClient = class {
4151
4234
  error: `Event rejected: ${response.code} - ${response.message}`
4152
4235
  };
4153
4236
  }
4237
+ if (response.data) {
4238
+ const httpResult = parseFulfillHttp(response.data);
4239
+ if (httpResult.isHttp && (httpResult.status < 200 || httpResult.status >= 300)) {
4240
+ const detail = httpResult.body ? ` - ${httpResult.body}` : "";
4241
+ return {
4242
+ success: false,
4243
+ error: `Write failed: relay returned HTTP ${httpResult.status} ${httpResult.statusText}`.trimEnd() + detail
4244
+ };
4245
+ }
4246
+ }
4154
4247
  return {
4155
4248
  success: true,
4156
4249
  eventId: event.id,
@@ -6483,6 +6576,8 @@ export {
6483
6576
  isTrustDowngrade,
6484
6577
  loadKeystore,
6485
6578
  parseBackupPayload,
6579
+ parseFulfillHttp,
6580
+ parseFulfillHttpBytes,
6486
6581
  parseHttpResponse,
6487
6582
  parsePetInteractionEvent,
6488
6583
  parsePetInteractionResult,