TypeScript SDK
The TypeScript SDK has the same features as the Go SDK. It is ESM, runs on Node.js 20 or later, and depends only on viem, as a peer dependency.
(cd <sdk>/ts && npm ci) # dependencies and the build in dist/npm install viem <sdk>/ts # in your projectWhat is inside
Section titled “What is inside”| Area | API |
|---|---|
| API client | new Client(...), with a method for each request of the API; errors are ApiError |
| Authorizations | authorizationDigest, authorizationFromSignature, parseRawSignature, normalizeS, verifyAuthorization |
| Revocations | revocationDigest, revocationFromSignature, verifyRevocation |
| Lists | sweeps, revocations, ledgerEntries, webhookDeliveries a page at a time; allSweeps and the like through every page |
| Webhooks | verifyWebhook and parseWebhook on Web Crypto; verifyWebhookSync and parseWebhookSync on node:crypto, from @portuna/sdk/node |
| Delegate check | verifyDeployment, factoryAddress, batcherAddress, delegateAddress, and the verify-delegate command |
| Types | every request and response of the API, with the API’s field names: Sweep, HotWallet, Event… |
Client
Section titled “Client”import { Client } from '@portuna/sdk'
const client = new Client({ baseUrl: 'https://api-testnet.portuna.io', apiKey: process.env.PORTUNA_API_KEY! })const balance = await client.balance({ signal: AbortSignal.timeout(10_000) })Options: baseUrl, apiKey and, optionally, your own fetch. Every method takes { signal } for cancellation
and timeouts. Methods: balance, tariff, settings, updateSettings, hotWallets, createHotWallet,
createSweep, sweep, sweeps, createRevocation, revocation, revocations, ledgerEntries, usage, webhook,
setWebhook, deleteWebhook, webhookDeliveries, sendTestWebhook, health and openapi. createSweep and
createRevocation return { sweep, created } and { revocation, created }: created is false when the
external_id was used before and the existing object came back.
sweeps, revocations, ledgerEntries and webhookDeliveries take a filter with the API’s query parameters, such
as { status: 'failed', limit: 100 }, and return one page, { items, next_cursor }. The all methods, allSweeps
and the like, go through every page as an async iterator:
for await (const sweep of client.allSweeps({ status: 'failed' })) { console.log(`sweep ${sweep.id} failed: ${sweep.reason}`)}Errors
Section titled “Errors”An answer other than 2xx throws ApiError with status, code and message; code is empty if the answer was not
the API’s error format, for example from a proxy. Codes and what to do: Errors and retries.
Authorizations
Section titled “Authorizations”authorizationDigest gives the digest your custody signs. authorizationFromSignature takes the raw signature, DER
or r‖s, as hex or bytes, with any s, normalizes it, finds y_parity and throws unless the deposit address signed.
Chain 0 is refused.
export async function signAuthorization( sign: Signer, keyId: string, deposit: Address, chainId: number, delegate: Address, nonce: number,): Promise<Authorization> { const digest = authorizationDigest({ chainId, address: delegate, nonce }) // refuses chain 0 const signature = await sign(keyId, digest) // Reads DER or r‖s, moves s to the lower half and finds y_parity. Throws unless the deposit address signed // this very digest, so a wrong key never reaches the API. return authorizationFromSignature({ chainId, address: delegate, nonce, deposit, signature })}More in Signing authorizations.
Webhooks
Section titled “Webhooks”parseWebhookSync(secret, header, body) from @portuna/sdk/node verifies the Portuna-Signature header
against the raw body, within five minutes, and returns the typed Event; it throws WebhookSignatureError otherwise.
Outside Node.js, use the async parseWebhook from @portuna/sdk.
function handle(event: Event): void { switch (event.type) { case 'sweep.swept': case 'sweep.failed': case 'sweep.rejected': // Events may come out of order: the status in the data is the current one. console.log(`sweep ${event.data.id} (${event.data.external_id}): ${event.data.status}`) break case 'balance.low': console.log(`top up the gas wallet on chain ${event.data.chain_id}: ${event.data.balance} wei left`) break default: // Other and future event types: accept them. }}
createServer((req, res) => { const chunks: Buffer[] = [] req.on('data', (chunk: Buffer) => chunks.push(chunk)) req.on('end', () => { // Verify the body exactly as it arrived, before parsing it. const body = Buffer.concat(chunks) let event: Event try { event = parseWebhookSync(secret, String(req.headers[signatureHeader.toLowerCase()] ?? ''), body) } catch { res.writeHead(401).end() return } // Delivery is at least once: an event may come again, with the same id. if (!handled.has(event.id)) { handle(event) handled.add(event.id) } res.writeHead(204).end() })}).listen(8080)Delegate check
Section titled “Delegate check”verifyDeployment takes any client with getChainId and getCode, such as a viem PublicClient:
const chain = createPublicClient({ transport: http(process.env.RPC_URL) })const report = await verifyDeployment(chain, { gasWallet: process.env.GAS_WALLET as Address, hotWallet: process.env.HOT_WALLET as Address, expectDelegate: process.env.DELEGATE as Address,})for (const c of report.checks) console.log(c.ok ? 'PASS' : 'FAIL', c.name, c.detail)if (!report.ok) throw new Error('do not sign authorizations for this delegate')The verify-delegate command does the same from the command line: see
Verifying the delegate.