API
Orders signieren
Wie ein Seeryx-Sitzungsschlüssel Orders signiert: die Order-Nachrichten PMO2, PMO3 und PMO4 und PMS1-Paare, Feld für Feld, mit TypeScript- und Python-Code.
Überblick
Jeder Trade auf seeryx.com wird über das Seeryx-Programm auf Solana abgewickelt, und das Programm akzeptiert nur Orders, die für dein Konto signiert sind. Deine Wallet signiert einmal, um eine Handelssitzung zu öffnen; danach signiert der Sitzungsschlüssel jede Order, Aufteilung und Zusammenlegung als kurze Binärnachricht. Der Server prüft die Signatur, bevor die Order ins Orderbuch gelangt, und das Programm prüft sie erneut, wenn die Ausführung abgewickelt wird.
- Einmal einzahlen. Dein Konto im Programm wird mit deiner ersten Einzahlung eröffnet (Einzahlungen und Abhebungen).
- Eine Sitzung öffnen mit einem eigenen Schlüssel: eine Wallet-Signatur, kein SOL.
- Jede Order signieren mit diesem Schlüssel und die Signatur mit der Order als
signedsenden.
Eine Handelssitzung öffnen
Erzeuge ein Ed25519-Schlüsselpaar, dann POST /api/relay:
| Feld | Typ | Beschreibung |
|---|---|---|
action | "session" | |
key | string | Der öffentliche Sitzungsschlüssel, Base58. |
expiresAt | number | Unix-Sekunden, höchstens maxSessionTtl von GET /api/funds ab jetzt. Die Beispiele verwenden dieses Limit minus 2 Minuten. |
limit | number | USDC, die die Sitzung insgesamt für Käufe und Aufteilungen ausgeben darf. |
maxTrade | number | USDC, die eine einzelne Order ausgeben darf; höchstens limit. |
Die Antwort ist { "transaction": "<base64>" }: eine Solana-v0-Transaktion mit Seeryx als Gebührenzahler und lauter leeren Signaturplätzen. Signiere sie mit der Wallet, ohne sie zu senden, und sende sie per POST /api/relay/submit: Seeryx fügt seine eigene Signatur hinzu und sendet sie. Warte, bis sie bestätigt ist (beliebiger Solana-RPC, getSignatureStatuses), und signiere dann Orders mit dem Schlüssel. tradingSession in chain.ts erledigt all das und speichert den Schlüssel in session.json.
Das signierte Objekt
| Feld | Typ | Beschreibung |
|---|---|---|
id | string | Eine u64-Order-ID als Dezimaltext, eindeutig für deine Wallet und nie wiederverwendet. Die Beispiele verwenden die Zeit in Millisekunden × 2^20 plus 20 Zufallsbits. |
limit | number | Der signierte Preis. Eine Limit-Order signiert ihr Limit, eine Market-Order ihr cap: ganze Cent in PMO2, Zehntelcent mit form: 3 oder form: 4. |
shares | string | Anteilseinheiten (1 Anteil = 10000) als Dezimaltext, in jeder Form außer PMO4. Eine Limit-Order oder ein Verkauf signiert genau ihre Anteile × 10000; ein PMO2/PMO3-Market-Kauf die meisten Einheiten, die sein Betrag zum cap inklusive Gebühr kauft. |
budget | string | Nur PMO4: der amount des Market-Kaufs in Micro-USDC als Dezimaltext, genau amount × 1000000. |
expiresAt | number | Unix-Sekunden: das Ende bei einer Limit-Order, 5 Minuten in der Zukunft (nie über das Ende hinaus) bei einer Market-Order. Siehe Ablauf. |
key | string | Der öffentliche Sitzungsschlüssel, Base58. |
signature | string | Die Ed25519-Signatur der unten beschriebenen Nachricht durch den Sitzungsschlüssel, 128 Hex-Zeichen. |
maxFeeBps | number | Der höchste Taker-Gebührensatz, den die Order akzeptiert: mindestens fees.rateBps von GET /api/funds (derzeit 700). |
form | 3 | 4 | 3: ein Limit zwischen ganzen Cent (PMO3). 4: ein Market-Kauf per Budget (PMO4). Fehlt bei PMO2. |
Nachrichtenformate
Der Sitzungsschlüssel signiert diese Bytes: ein 4-Byte-ASCII-Präfix und die Felder der Reihe nach, Ganzzahlen Little-Endian (borsh). Wähle die Form nach der Order:
| Orderart | Form | Bytes |
|---|---|---|
| Limit-Order oder Verkauf zu einem ganzen Cent; Market-Verkauf | PMO2 | 97 |
| Limit-Order oder Verkauf zwischen ganzen Cent (unter 1¢, über 99¢) | PMO3 | 98 |
Market-Kauf, wo budgetBuys von GET /api/funds aktiv ist (auf seeryx.com ist es das) | PMO4 | 97 |
| Market-Kauf, wo budgetBuys nicht aktiv ist | PMO2 oder PMO3, Anteile aus dem Betrag | |
| Aufteilen oder Zusammenlegen | PMS1 | 93 |
PMO2: eine Order zu einem ganzen Cent
| Offset | Bytes | Feld | Kodierung |
|---|---|---|---|
| 0 | 4 | prefix | "PMO2" (ASCII) |
| 4 | 32 | owner | der öffentliche Schlüssel deiner Wallet |
| 36 | 32 | market | das Marktkonto |
| 68 | 8 | id | u64 |
| 76 | 1 | side | 0 Steigt (up), 1 Fällt (down) |
| 77 | 1 | action | 0 Kauf (buy), 1 Verkauf (sell) |
| 78 | 1 | limit | u8, ganze Cent 1–99 |
| 79 | 8 | shares | u64, Anteilseinheiten (1 Anteil = 10000) |
| 87 | 8 | expiresAt | i64, Unix-Sekunden |
| 95 | 2 | maxFeeBps | u16 |
PMO3: eine Order zwischen ganzen Cent
| Offset | Bytes | Feld | Kodierung |
|---|---|---|---|
| 0 | 4 | prefix | "PMO3" |
| 4 | 32 | owner | |
| 36 | 32 | market | |
| 68 | 8 | id | u64 |
| 76 | 1 | side | 0 Steigt (up), 1 Fällt (down) |
| 77 | 1 | action | 0 Kauf (buy), 1 Verkauf (sell) |
| 78 | 2 | limit | u16, Zehntelcent (995 sind 99,5¢) |
| 80 | 8 | shares | u64 |
| 88 | 8 | expiresAt | i64 |
| 96 | 2 | maxFeeBps | u16 |
PMO4: ein Market-Kauf per Budget
Keine Aktion (immer ein Kauf) und keine Anteilszahl. Das Programm hält die Order über alle Ausführungen an ihr Budget, sie gibt also ihren ganzen Betrag bis zum cap aus, egal wie weit unter dem cap sie ausgeführt wird.
| Offset | Bytes | Feld | Kodierung |
|---|---|---|---|
| 0 | 4 | prefix | "PMO4" |
| 4 | 32 | owner | |
| 36 | 32 | market | |
| 68 | 8 | id | u64 |
| 76 | 1 | side | 0 Steigt (up), 1 Fällt (down) |
| 77 | 2 | limit | u16, Zehntelcent: der schlechteste Preis jeder Ausführung |
| 79 | 8 | budget | u64, Micro-USDC: der Höchstbetrag insgesamt, Gebühr inklusive |
| 87 | 8 | expiresAt | i64 |
| 95 | 2 | maxFeeBps | u16 |
PMS1: eine Aufteilung oder Zusammenlegung
Ihr signed ist { id, shares, expiresAt, key, signature }; bei einer Aufteilung ist shares der Betrag × 10000.
| Offset | Bytes | Feld | Kodierung |
|---|---|---|---|
| 0 | 4 | prefix | "PMS1" |
| 4 | 32 | owner | |
| 36 | 32 | market | |
| 68 | 8 | id | u64 |
| 76 | 1 | merge | 0 Aufteilen, 1 Zusammenlegen |
| 77 | 8 | shares | u64, Paare in Anteilseinheiten |
| 85 | 8 | expiresAt | i64 |
Ein PMO2/PMO3-Market-Kauf darf nie mehr kosten können als sein Betrag: Der Server prüft units × limit / 10 + fee(units, limit) ≤ amount × 1000000 (limit in Zehntelcent, Gebühr auf ein Micro-USDC abgerundet). affordable in message.ts findet die größten solchen units. Eine solche Order stoppt, sobald sie diese Einheiten hat (reason: "share_limit"), und der Rest des Betrags bleibt in deinem Guthaben; PMO4 stoppt dort nie.
Das Marktkonto
market ist die Programmadresse, die aus programId von GET /api/funds mit den Seeds "market" und dem Slug als UTF-8-Bytes (mit Nullen auf 32 aufgefüllt) abgeleitet wird. owner ist der öffentliche Schlüssel deiner Wallet, nicht der Sitzungsschlüssel.
const id = new Uint8Array(32);
id.set(new TextEncoder().encode(slug));
const [market] = await getProgramDerivedAddress({ programAddress: address(funds.programId), seeds: ['market', id] });Code
message.ts baut die Bytes (keine Abhängigkeiten; Byte für Byte gegen die eigene Kodierung des Programms geprüft). chain.ts öffnet die Sitzung, signiert Orders, Aufteilungen und Zusammenlegungen und sendet vorbereitete Transaktionen.
// message.ts: the bytes a session key signs, exactly as the program reads them (borsh, little-endian).
// No dependencies. tests/developers.test.ts checks it against tests/fixtures/messages.json.
const le = (size: number, value: bigint) => {
const out = new Uint8Array(size);
for (let i = 0; i < size; i++) out[i] = Number((value >> BigInt(8 * i)) & 0xffn);
return out;
};
const join = (...parts: Uint8Array[]) => {
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
let at = 0;
for (const p of parts) { out.set(p, at); at += p.length; }
return out;
};
const ascii = (text: string) => new TextEncoder().encode(text);
export interface OrderTerms {
owner: Uint8Array; // your wallet's public key, 32 bytes
market: Uint8Array; // the round's market account, 32 bytes
id: bigint; // unique per wallet, never reused
side: 'up' | 'down';
action: 'buy' | 'sell';
limit: number; // tenths of a cent: 450 is 45c, 995 is 99.5c
shares: bigint; // share units: 1 share is 10000
expiresAt: bigint; // unix seconds, at most the round's end
maxFeeBps: number; // the highest taker fee rate you accept
}
// PMO2 for a limit in whole cents, PMO3 for a limit between whole cents.
export function orderMessage(o: OrderTerms) {
const side = o.side === 'up' ? 0 : 1, action = o.action === 'buy' ? 0 : 1;
const head = join(o.owner, o.market, le(8, o.id), Uint8Array.of(side, action));
const tail = join(le(8, o.shares), le(8, o.expiresAt), le(2, BigInt(o.maxFeeBps)));
return o.limit % 10 === 0
? join(ascii('PMO2'), head, le(1, BigInt(o.limit / 10)), tail)
: join(ascii('PMO3'), head, le(2, BigInt(o.limit)), tail);
}
// PMO4: a market buy by budget, where GET /api/funds says budgetBuys. It spends up to `budget` micro-USDC in all,
// taker fee included, and never fills above `limit` (tenths of a cent); no action (always a buy), no share count.
export function budgetMessage(o: {
owner: Uint8Array; market: Uint8Array; id: bigint; side: 'up' | 'down';
limit: number; budget: bigint; expiresAt: bigint; maxFeeBps: number;
}) {
const side = Uint8Array.of(o.side === 'up' ? 0 : 1);
return join(ascii('PMO4'), o.owner, o.market, le(8, o.id), side, le(2, BigInt(o.limit)),
le(8, o.budget), le(8, o.expiresAt), le(2, BigInt(o.maxFeeBps)));
}
// PMS1: a split (cash into Up + Down pairs) or a merge (pairs back into cash).
export function pairMessage(p: {
owner: Uint8Array; market: Uint8Array; id: bigint; merge: boolean; shares: bigint; expiresAt: bigint;
}) {
const merge = Uint8Array.of(p.merge ? 1 : 0);
return join(ascii('PMS1'), p.owner, p.market, le(8, p.id), merge, le(8, p.shares), le(8, p.expiresAt));
}
// The most share units `cash` micro-USDC buys at `price` tenths of a cent, taker fee included:
// what a market buy signs.
export function affordable(cash: bigint, price: number, rateBps: number) {
const step = price % 10 ? 10n : 1n; // between whole cents shares go in steps of 0.001
const p = BigInt(price), rate = BigInt(rateBps);
const cost = (n: bigint) => n * p / 10n + n * rate * p * (1000n - p) / 100_000_000n;
let n = cash * 100_000_000n / (p * 10_000_000n + rate * p * (1000n - p));
n -= n % step;
while (n > 0n && cost(n) > cash) n -= step;
while (cost(n + step) <= cash) n += step;
return n;
}// chain.ts: trading session and signed orders for on-chain trading. Node 22+, npm i @solana/kit
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import {
address, createKeyPairFromPrivateKeyBytes, createSolanaRpc, getAddressEncoder, getAddressFromPublicKey,
getBase64EncodedWireTransaction, getBase64Encoder, getProgramDerivedAddress,
getTransactionDecoder, partiallySignTransaction, signBytes,
} from '@solana/kit';
import type { Address, Signature } from '@solana/kit';
import { affordable, budgetMessage, orderMessage, pairMessage } from './message';
import { api, type Wallet } from './seeryx';
// GET /api/funds on a server that trades on chain
export interface Funds {
mode: string; onChainTrading?: boolean; cluster: 'mainnet' | 'devnet';
programId: string; maxSessionTtl: number; fees: { rateBps: number } | null;
budgetBuys?: boolean; // market buys are signed by budget (PMO4)
}
export interface Session { key: Address; expiresAt: number; pair: CryptoKeyPair }
export interface Order {
slug: string; side: 'up' | 'down'; action: 'buy' | 'sell'; type: 'limit' | 'market';
shares?: number; amount?: number; limit?: number; cap?: number; postOnly?: boolean;
}
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const bytes = (a: Address) => new Uint8Array(getAddressEncoder().encode(a));
const hex = (b: Uint8Array) => Buffer.from(b).toString('hex');
// A transaction the exchange prepared with itself as the fee payer: the wallet signs first (every other slot is empty).
export async function signPrepared(wallet: Wallet, base64: string) {
const transaction = getTransactionDecoder().decode(getBase64Encoder().encode(base64));
return partiallySignTransaction([wallet.pair], transaction);
}
// Any Solana RPC of the cluster reads the transaction's status: your own in SOLANA_RPC, or the public one.
const rpcOf = (funds: Funds) => createSolanaRpc(process.env.SOLANA_RPC
?? (funds.cluster === 'mainnet' ? 'https://api.mainnet-beta.solana.com' : 'https://api.devnet.solana.com'));
// Signs it, sends it and waits until it is confirmed. The wallet needs no SOL.
export async function sendPrepared(funds: Funds, wallet: Wallet, base64: string) {
const signed = await signPrepared(wallet, base64);
// the exchange adds its own signature after the wallet's and sends it
const transaction = getBase64EncodedWireTransaction(signed);
const sent = await api<{ signature: string }>('/relay/submit', { method: 'POST', body: { transaction } });
const signature = sent.signature as Signature;
const rpc = rpcOf(funds);
for (let i = 0; i < 60; i++) {
await sleep(1000);
const { value: [status] } = await rpc.getSignatureStatuses([signature]).send();
if (status?.err) throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
const state = status?.confirmationStatus;
if (state === 'confirmed' || state === 'finalized') return signature;
}
throw new Error('The transaction was not confirmed in time');
}
// The session key signs orders without the wallet. It can trade within its limits and can never withdraw.
// Kept in session.json (keep that file private) and reused while it has time left. A limit order rests until its
// round closes and must not outlive the session: `until` is the latest close you will quote in (unix seconds).
export async function tradingSession(funds: Funds, wallet: Wallet, budget = 1000, until = 0): Promise<Session> {
const file = 'session.json', now = Math.floor(Date.now() / 1000);
if (existsSync(file)) {
const saved = JSON.parse(readFileSync(file, 'utf8'));
// the furthest 5m and 15m rounds that trade close within 3 hours: keep a session only with time to spare
if (saved.owner === wallet.address && saved.expiresAt > Math.max(now + 3 * 3600, until + 60)) {
const pair = await createKeyPairFromPrivateKeyBytes(new Uint8Array(saved.seed));
return { key: saved.key, expiresAt: saved.expiresAt, pair };
}
}
const seed = crypto.getRandomValues(new Uint8Array(32));
const pair = await createKeyPairFromPrivateKeyBytes(seed);
const key = await getAddressFromPublicKey(pair.publicKey);
const expiresAt = now + funds.maxSessionTtl - 120;
// budget: USDC this session may spend on buys and splits in total, and per order
const body = { action: 'session', key, expiresAt, limit: budget, maxTrade: budget };
const { transaction } = await api<{ transaction: string }>('/relay', { method: 'POST', body });
await sendPrepared(funds, wallet, transaction);
const record = { owner: wallet.address, key, expiresAt, seed: [...seed] };
writeFileSync(file, JSON.stringify(record), { mode: 0o600 });
return { key, expiresAt, pair };
}
// The round's market account: the slug, zero-padded to 32 bytes, is the seed.
export async function marketOf(funds: Funds, slug: string) {
const id = new Uint8Array(32);
id.set(new TextEncoder().encode(slug));
const programAddress = address(funds.programId);
const [market] = await getProgramDerivedAddress({ programAddress, seeds: ['market', id] });
return market;
}
// When a market closes (unix seconds): a round at start + length, an event market at its closesAt.
const LENGTH: Record<string, number> = { '5m': 300, '15m': 900, '1h': 3600, '1d': 86_400 };
const closes = new Map<string, number>();
export async function closeOf(slug: string) {
const [, tf, start] = /-updown-(5m|15m|1h|1d)-(\d+)$/.exec(slug) ?? [];
if (tf) return Number(start) + LENGTH[tf];
if (!closes.has(slug)) closes.set(slug, (await api<{ closesAt: number }>(`/events/${slug}`)).closesAt);
return closes.get(slug)!;
}
// A limit order is signed until the close; a market order, split or merge for 5 minutes (never past the close).
async function expiry(slug: string, rests: boolean) {
const close = await closeOf(slug);
return rests ? close : Math.min(close, Math.floor(Date.now() / 1000) + 300);
}
// Order ids: unique per wallet and never reused: the time in milliseconds x 2^20 plus 20 random bits.
let lastId = 0n;
function nextId() {
const random = BigInt(crypto.getRandomValues(new Uint32Array(1))[0] & 0xfffff);
const id = (BigInt(Date.now()) << 20n) | random;
lastId = id > lastId ? id : lastId + 1n;
return lastId;
}
// The order body with its `signed` object, ready for POST /api/orders (or one item of /api/orders/batch).
export async function signOrder(funds: Funds, session: Session, owner: Address, order: Order) {
const price = order.type === 'limit' ? order.limit : order.cap; // market orders must carry a cap
if (price === undefined) throw new Error('A limit order needs limit, a market order needs cap');
const limit = Math.round(price * 10); // tenths of a cent
const rate = funds.fees?.rateBps ?? 0;
if (order.type === 'market' && order.action === 'buy' && funds.budgetBuys) {
// PMO4: the amount itself is signed as the budget (fee included) and the cap as the worst price of any fill
const id = nextId(), expiresAt = await expiry(order.slug, false), budget = BigInt(Math.round(order.amount! * 1e6));
const market = bytes(await marketOf(funds, order.slug));
const message = budgetMessage({
owner: bytes(owner), market, id, side: order.side, limit, budget, expiresAt: BigInt(expiresAt), maxFeeBps: rate,
});
const signature = hex(await signBytes(session.pair.privateKey, message));
const signed = { id: String(id), limit, budget: String(budget), expiresAt, key: session.key, signature, maxFeeBps: rate, form: 4 };
return { ...order, signed };
}
// a market buy (where budgetBuys is off) signs what its amount buys at the cap, fee included; everything else its shares
const shares = order.type === 'market' && order.action === 'buy'
? affordable(BigInt(Math.round(order.amount! * 1e6)), limit, rate)
: BigInt(Math.round(order.shares! * 1e4));
const id = nextId(), expiresAt = await expiry(order.slug, order.type === 'limit');
const market = bytes(await marketOf(funds, order.slug));
const message = orderMessage({
owner: bytes(owner), market, id, side: order.side, action: order.action,
limit, shares, expiresAt: BigInt(expiresAt), maxFeeBps: rate,
});
const signature = hex(await signBytes(session.pair.privateKey, message));
const fine = limit % 10 !== 0; // between whole cents: PMO3, the limit in tenths
const signed = {
id: String(id), limit: fine ? limit : limit / 10, shares: String(shares), expiresAt,
key: session.key, signature, maxFeeBps: rate, ...(fine ? { form: 3 } : {}),
};
return { ...order, signed };
}
// `signed` of a split (`pairs` = the amount in USDC) or a merge (`pairs` = the shares).
export async function signPair(
funds: Funds, session: Session, owner: Address, slug: string, merge: boolean, pairs: number,
) {
const id = nextId(), expiresAt = await expiry(slug, false), shares = BigInt(Math.round(pairs * 1e4));
const market = bytes(await marketOf(funds, slug));
const message = pairMessage({
owner: bytes(owner), market, id, merge, shares, expiresAt: BigInt(expiresAt),
});
const signature = hex(await signBytes(session.pair.privateKey, message));
return { id: String(id), shares: String(shares), expiresAt, key: session.key, signature };
}Eine signierte Anfrage
{
"slug": "btc-updown-5m-1791916200", "side": "up", "action": "buy", "type": "limit", "shares": 10, "limit": 45,
"signed": {
"id": "1877965812345678901", "limit": 45, "shares": "100000", "expiresAt": 1791916500,
"key": "GmaDrppBC7P5ARKV8g3djiwP89vz1jLK23V2GBjuAEGB",
"signature": "2c6e257e85a2df1d...9e8a0f (128 hex characters)",
"maxFeeBps": 700
}
}
{
"slug": "btc-updown-5m-1791916200", "side": "down", "action": "buy", "type": "market", "amount": 5, "cap": 60,
"signed": {
"id": "1877965812345678902", "limit": 600, "budget": "5000000", "expiresAt": 1791916460,
"key": "GmaDrppBC7P5ARKV8g3djiwP89vz1jLK23V2GBjuAEGB", "signature": "...", "maxFeeBps": 700, "form": 4
}
}Wenn der Server ablehnt
| 400-Fehler | Lösung |
|---|---|
| Orders must be signed by your trading session; reload the page | Füge signed hinzu. |
| Malformed signed order | Ein Feld hat den falschen Typ: id, shares und budget sind Dezimaltext; limit, expiresAt und maxFeeBps ganze Zahlen. |
| The signed order must expire by the round close | expiresAt liegt in der Vergangenheit oder nach dem Ende. |
| The signed order expires too soon to settle; sign it again | expiresAt liegt weniger als 5 Sekunden in der Zukunft. |
| Your trading session expired; sign in to trading again | key ist nicht deine Sitzung auf der Blockchain, oder sie endet innerhalb von 5 Sekunden: Öffne eine neue Sitzung. |
| Your trading session ends before this order would; sign in to trading again | Eine Limit-Order würde über das Ende der Sitzung hinaus im Orderbuch liegen. |
| The order signature is not valid | Die Bytes, der owner, der market oder der key weichen von dem ab, was der Server aus der Order rekonstruiert hat. |
| The signed order does not match the order | Das signierte Limit oder die signierten Anteile weichen von denen der Order ab. |
| The signed order exceeds the amount | Ein PMO2/PMO3-Market-Kauf hat mehr Anteile signiert, als sein Betrag zum cap bezahlt. |
| The signed order does not match the amount | Ein PMO4-Budget ist nicht amount × 1000000. |
| Only a market buy is signed with a budget | form 4 bei einer Limit-Order oder einem Verkauf. |
| Market buys by budget are not available yet; reload the page | Eine PMO4-Order, wo budgetBuys nicht aktiv ist. |
| Trading fees changed; reload the page and place the order again | maxFeeBps liegt unter dem aktuellen Satz: Lies GET /api/funds erneut. |
| This order was already submitted | Die id wurde schon verwendet: Signiere jeden neuen Versuch mit einer neuen id. |
| An order can spend at most $… in this trading session | Die Order liegt über dem maxTrade der Sitzung. |
| Your trading session limit is used up; sign in to trading again | Öffne eine neue Sitzung, bei Bedarf mit einem höheren limit. |
Trading is paused after secure disconnect... (trading_paused) | Der Schlüssel wurde geschlossen: Öffne eine neue Sitzung und signiere erneut. |