Skip to content
PortunaPortunaPortunaDocsTestnet

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.

Terminal window
(cd <sdk>/ts && npm ci) # dependencies and the build in dist/
npm install viem <sdk>/ts # in your project
AreaAPI
API clientnew Client(...), with a method for each request of the API; errors are ApiError
AuthorizationsauthorizationDigest, authorizationFromSignature, parseRawSignature, normalizeS, verifyAuthorization
RevocationsrevocationDigest, revocationFromSignature, verifyRevocation
Listssweeps, revocations, ledgerEntries, webhookDeliveries a page at a time; allSweeps and the like through every page
WebhooksverifyWebhook and parseWebhook on Web Crypto; verifyWebhookSync and parseWebhookSync on node:crypto, from @portuna/sdk/node
Delegate checkverifyDeployment, factoryAddress, batcherAddress, delegateAddress, and the verify-delegate command
Typesevery request and response of the API, with the API’s field names: Sweep, HotWallet, Event…
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}`)
}

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.

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.

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)

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.