Build a game
Voucher signing
How your game server signs item, state and trophy vouchers with its game key, the exact bytes that are signed, replay and expiry rules, key rotation and a test vector.
A voucher is a statement your game server signs: "this player earned this item", "this item's stats are now these", or "this player unlocked this trophy". The player's launcher submits it to the chain, and the contract checks your signature against the public key it holds for your game. Gas is paid by the game's budget where one exists.
Rare never holds your key and never signs for you. A leaked key can mint items up to the on-chain supply caps, so keep it on your server, out of git and out of the client.
The item and state formats below are the rare_items design, which is not published yet. The trophy format is the one in the Trophies contract. All three are signed the same way.
The game key
Studio generates an ed25519 key in your browser, offers the private key as a one-time download and puts only the public key on chain: in the item class (issuer) and the trophies game (set_issuer). Load the private key on your server:
import { gameKey } from "@rare/sdk/server";
const key = gameKey(process.env.RARE_GAME_KEY!);
console.log(key.publicKeyHex); // 32 bytes, the same value that is on chaingameKey accepts a suiprivkey1... string, 64 hex characters (with or without 0x), 32 bytes of base64 or the raw bytes. A key of another scheme is refused.
Making vouchers
import { itemVoucher } from "@rare/sdk/server";
const v = await itemVoucher(key, {
classId: process.env.CLASS_ID!,
player, // the player's address
kind: 3, // the kind id from add_kind
attrs: { power: 12 },
ttlMs: 600_000, // default 10 minutes
});
res.json({ voucher: v.token });Send v.token to the game client and call rare.claim(token). The token is the base64url of a JSON envelope with the fields and the signature, so it is safe in a URL (rare://claim?v=<token>) as well as in JSON.
What is signed
The signature is ed25519 over raw bytes: no hashing and no intent prefix. All integers are u64 little endian, addresses and object ids are 32 bytes, and bcs(keys) is the BCS encoding of a vector of strings, exactly what Move's bcs::to_bytes writes.
The class id and registry id bind a voucher to one deployment, so a testnet voucher cannot be used on mainnet. A state voucher signs keys and vals and the contract writes only the keys it is given, so you can change one stat without re-signing the rest.
Replay, expiry and ordering
- Expiry.
expires_msis checked against the chain clock. Keep it short. The default is 10 minutes. - Replay. The nonce is recorded per player. A voucher with a nonce the player already used is refused. The default nonce is 64 random bits, so two vouchers never collide. Pass your own
nonceif you want vouchers you can reason about, but never reuse one for the same player. - Ordering. A state voucher carries
revand the contract requires it to equal the item's currentrev, then increments it. A stale or replayed update cannot apply, and updates apply in order. - Player binding. Item and trophy vouchers name the player. Only that address can use them.
Verify what you sign
verifyVoucher checks a voucher the way the chain will. Use it in tests and in any service that accepts vouchers from another service.
import { verifyVoucher } from "@rare/sdk/server";
const v = await verifyVoucher(token, key.publicKey, { player });It throws a RareVoucherError (code voucher_invalid) for a malformed token, a signature that does not verify, an expired voucher or the wrong player.
Rotating the key
Generate a new key in Studio and call set_issuer with its public key (on the item class and on the trophies game). Vouchers signed with the old key stop working at once. Vouchers still in flight fail with rejected, and the player simply asks the server for a new one. Setting an empty key turns vouchers off.
Test vector
Use this to check another language's implementation. The private key is 31 zero bytes and then 01.
524152455f4954454d5f5631
c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1
0909090909090909090909090909090909090909090909090909090909090909
0300000000000000
0105706f776572
01023132
0700000000000000
c0776518a30100000c429736bcdb62babb3ac80cca7108d51a3e02a46bf335c7ff401e067e1f2226fb12d888f34dc40101afdd19529552c7a91292f0343efd11e90ade19c96f3d0aThe line breaks in the message are only for reading. It is one byte string.
