npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@swype-org/withdrawals

v0.1.4

Published

Lightweight merchant withdrawals SDK — quote a cross-chain/cross-token withdrawal, sign with your own wallet, report and track to completion, zero runtime dependencies

Downloads

108

Readme

@swype-org/withdrawals

Lightweight merchant withdrawals SDK — quote a withdrawal, sign the returned transactions with your own wallet, report the broadcast hashes, and track to completion. Zero runtime dependencies, fully signer-agnostic.

A withdrawal moves funds out of a user's embedded wallet to a destination (chain / token / address). It covers a plain withdrawal (same chain, new recipient), a cross-chain or cross-token move, and a migration (e.g. USDM on one chain → USDC on another) — all the same lifecycle.

Requirements

Node 16+ or a modern browser — the client relies only on the fetch, AbortController, btoa, and TextEncoder globals available in those runtimes. Zero runtime dependencies; ships both ESM and CommonJS builds with TypeScript types.

Quick Start

# Published package
npm install @swype-org/withdrawals

# Or from a monorepo checkout
npm install file:../path/to/withdrawals-sdk
import { WithdrawalsClient } from '@swype-org/withdrawals';

const client = new WithdrawalsClient({
  baseUrl: 'https://api.blink.cash',
  // Called before every request — return a freshly-signed envelope so a
  // long-lived client never sends a stale (expired) credential.
  merchantAuthorization: async () => myBackend.signMerchantEnvelope(),
});

// 1. Quote the withdrawal.
const quote = await client.createWithdrawal({
  source: { chainId: 4326, tokenAddress: '0x...', wallet: '0xUserEmbeddedWallet' },
  destination: { chainId: 1337, address: '0xRecipient', token: { address: '0x...' } },
  amount: '1000000000000000000', // source token's smallest unit, as a string
  idempotencyKey: 'a-stable-uuid', // optional; same key returns the same withdrawal
});

// 2. Sign + broadcast each returned transaction with your own wallet
//    (e.g. a Privy server SDK delegated session signer).
const reported = [];
for (const tx of quote.transactions) {
  // tx.kind is 'transaction' (broadcast) or 'signature' (sign a message — no broadcast).
  const { hash } = await myPrivyClient.signAndSendTransaction(/* tx.to, tx.data, tx.value, tx.chainId */);
  reported.push({ txHash: hash, chainId: tx.chainId, stepIndex: tx.stepIndex });
}

// 3. Report the broadcast hashes.
await client.reportTransactions(quote.withdrawalId, reported);

// 4. Poll status to completion.
let withdrawal = await client.getWithdrawal(quote.withdrawalId);
while (withdrawal.status !== 'completed' && withdrawal.status !== 'failed') {
  await new Promise((r) => setTimeout(r, 5_000));
  withdrawal = await client.getWithdrawal(quote.withdrawalId);
}

How It Works

┌─────────────┐   createWithdrawal    ┌──────────────┐
│  Merchant   │ ────────────────────▶ │  blink API   │  quotes the route,
│  backend    │ ◀──────────────────── │              │  returns unsigned txs
└─────────────┘   transactions[]      └──────────────┘
       │
       │ sign + broadcast each tx with your own wallet
       │ (e.g. Privy delegated session signer)
       ▼
┌─────────────┐  reportTransactions   ┌──────────────┐
│  Merchant   │ ────────────────────▶ │  blink API   │  tracks to completion
│  backend    │      getWithdrawal    │              │  (poll status)
└─────────────┘ ◀──────────────────── └──────────────┘

The SDK is signer-agnostic: it returns neutral, unsigned transactions[] and you sign them with whatever wallet you use.

Signing prerequisite (delegated session signer)

For the backend / no-popup flow (the typical withdrawal case), the user must have delegated a session signer to you once on the client (a revocable, one-time consent step in your app). Your backend can then sign on behalf of their embedded wallet with no user present. See your wallet provider's delegated-signing docs.

Data returned

The quote (createWithdrawal):

  • withdrawalId — reference it in every later call.
  • transactions[] — the neutral, unsigned steps you sign + broadcast. Each has a stepIndex, a kind ('transaction' to broadcast, 'signature' to sign a message), and to / data / value / chainId.
  • estimatedOutput / estimatedFeeUsd — string estimates, or null when unavailable.
  • quoteValidUntil — ISO-8601 server-side TTL (see Notes).

The withdrawal record (reportTransactions, getWithdrawal):

  • status'quoted' | 'submitted' | 'routing' | 'completed' | 'failed'.
  • submittedTxHashes[] — hashes the API has observed for this withdrawal.
  • destinationTxHash — the destination-chain hash, populated on 'completed' (else null).
  • failureReason — populated on 'failed' (else null).
  • createDate / updateDate — ISO-8601 timestamps.

Authorization

merchantAuthorization is a function returning a freshly-signed envelope { merchantId, payload, signature }. The SDK calls it before each request and sends it base64-encoded in the X-Merchant-Authorization header. This envelope is an API credential (it identifies you as the merchant) — it is not what authorizes funds to move; that authority is the user's delegated session signer. Sign the envelope on your backend; the SDK never signs it.

Envelope format

| Field | Contents | |--------------|---------------------------------------------------------------------------------------------------| | merchantId | Your merchant ID, as registered with Blink. | | payload | base64url-encoded JSON: { "version": "1", "signatureTimestamp": <ISO-8601> } — when you signed, in ISO-8601. | | signature | base64url-encoded ECDSA P-256 / SHA-256 signature (DER-encoded) over the raw base64url payload string — sign the encoded string itself, not the decoded JSON. |

Expiry is enforced server-side: the API rejects any envelope whose signatureTimestamp is more than 15 minutes old or in the future (30s clock-skew tolerance). You don't declare an expiry — just sign fresh.

Generating a signing key

The API verifies your signature against the PEM public key you registered with Blink, using the ECDSA_P256_SHA256 algorithm. Generate a P-256 keypair and share only the public half during merchant registration:

openssl ecparam -name prime256v1 -genkey -noout -out merchant-signing-key.pem
openssl ec -in merchant-signing-key.pem -pubout -out merchant-signing-key.pub.pem

Keep the private key on your backend (e.g. in a secrets manager) — never ship it to a client.

Example: signing the envelope (Node)

import { createSign } from 'node:crypto';
import type { MerchantAuthorizationEnvelope } from '@swype-org/withdrawals';

const MERCHANT_ID = 'your-merchant-id';
const PRIVATE_KEY_PEM = process.env.MERCHANT_SIGNING_KEY!; // P-256 private key, PEM

export function signMerchantEnvelope(): MerchantAuthorizationEnvelope {
  // 1. Build the payload and base64url-encode it.
  const payload = Buffer.from(
    JSON.stringify({
      version: '1',
      signatureTimestamp: new Date().toISOString(),
    }),
    'utf8',
  ).toString('base64url');

  // 2. Sign the base64url string itself (its UTF-8 bytes), ECDSA P-256 + SHA-256.
  //    Node produces a DER-encoded signature by default, which is what the API expects.
  const signer = createSign('SHA256');
  signer.update(payload);
  signer.end();
  const signature = signer.sign(PRIVATE_KEY_PEM).toString('base64url');

  return { merchantId: MERCHANT_ID, payload, signature };
}

const client = new WithdrawalsClient({
  baseUrl: 'https://api.blink.cash',
  merchantAuthorization: signMerchantEnvelope, // fresh signature per request
});

Any signer that produces a DER-encoded ECDSA P-256 / SHA-256 signature works — e.g. AWS KMS with ECDSA_SHA_256 (KMS returns DER) — as long as the registered public key matches. Signing fresh per request is cheap and avoids ever hitting the expiry window; if you cache an envelope, stay well inside the 15-minute signatureTimestamp age limit.

If the envelope is rejected, the API responds with codes like MERCHANT_AUTHORIZATION_EXPIRED, MERCHANT_SIGNATURE_INVALID, MERCHANT_PAYLOAD_INVALID, or MERCHANT_NOT_REGISTERED (surfaced by the SDK as a WithdrawalError with code REQUEST_FAILED).

Configuration

new WithdrawalsClient({
  baseUrl: 'https://api.blink.cash',      // required
  merchantAuthorization: () => envelope,  // required — sync or async, called per request
  fetch,                                  // optional — defaults to global fetch
  timeoutMs: 15000,                       // optional — per-request timeout (default 15s)
});

Notes

  • quoteValidUntil is a blink server-side TTL indicating how long the quote stays valid — re-quote if it expires before you broadcast.
  • reportTransactions is idempotent — replaying the same hashes is harmless.

Error Handling

Every failure throws a WithdrawalError with a machine-readable code:

| Code | Meaning | |--------------------------|----------------------------------------------------------------------| | INVALID_REQUEST | Bad input passed to the client (e.g. missing withdrawalId). | | AMOUNT_TOO_LOW | The amount is too low to cover network/bridge fees — try a larger amount. | | ROUTE_UNSUPPORTED | The source/destination chain or token can't be bridged. | | INSUFFICIENT_LIQUIDITY | Not enough liquidity for this amount right now — try smaller or later. | | INSUFFICIENT_FUNDS | The source wallet has insufficient balance for this withdrawal. | | REQUEST_FAILED | The server rejected the request (other non-2xx). | | NETWORK_ERROR | The server was unreachable. | | TIMEOUT | The request exceeded timeoutMs. | | RESPONSE_INVALID | The server returned an unexpected response shape. |

import { WithdrawalError, getDisplayMessage } from '@swype-org/withdrawals';

try {
  await client.createWithdrawal(input);
} catch (err) {
  if (err instanceof WithdrawalError) {
    if (err.code === 'AMOUNT_TOO_LOW') {
      // e.g. prompt the user to increase the amount
    }
    console.error(err.code, getDisplayMessage(err));
  }
}

React (reserved)

A @swype-org/withdrawals/react subpath is reserved for a future client-side popup-signer flow (sign in the user's browser). It is not built yet; the package ships the backend client only.

Exports

Values: WithdrawalsClient, WithdrawalError, getDisplayMessage, VERSION.

Types: WithdrawalsClientConfig, CreateWithdrawalInput, WithdrawalQuote, Withdrawal, WithdrawalTransaction, ReportedTransaction, WithdrawalSource, WithdrawalDestination, WithdrawalKind, WithdrawalStatus, WithdrawalErrorCode, MerchantAuthorizationEnvelope, MerchantAuthorizationProvider, FetchLike.