Skip to content

@findloot/sdkv0.1.1MITStellar testnet

Ship puzzle chests
from TypeScript.

Create, read and claim Loot chests on Stellar. Answers stay on the device; proofs are bound to the solver's address and verified on Soroban.

$ npm i @findloot/sdk

Quickstart

From install to a live chest.

Three calls. The answer is hashed on the device; only the fingerprint reaches the ledger.

  1. 1

    Install

    One ESM package. It pulls in @stellar/stellar-sdk and circomlibjs.

    terminalShell
    $ npm i @findloot/sdk
  2. 2

    Create a chest

    Hash the answer on the device, then escrow XLM behind the fingerprint. Returns the chest id.

    create.tsTypeScript
    import {  createChestClient,  fingerprintAnswer,  CONTRACTS,  DEFAULT_AMOUNT_PER_SLOT_STROOPS,} from "@findloot/sdk";// 1. Hash the answer locally — only the fingerprint goes on-chainconst { fingerprintHex } = await fingerprintAnswer("alexandria", 7n);// 2. Escrow 10 XLM behind itconst client = createChestClient({ publicKey: creator });const tx = await client.create_chest({  creator,  fingerprint: Buffer.from(fingerprintHex, "hex"),  token: CONTRACTS.xlm,  prize: DEFAULT_AMOUNT_PER_SLOT_STROOPS,  max_claims: 1,  deadline: BigInt(Math.floor(Date.now() / 1000) + 7 * 86400),});// signer passed at send time (wiring unverified)const sent = await tx.signAndSend({ signTransaction });const chestId = sent.result.unwrap();
  3. 3

    Solve and claim

    Read the chest, prove the answer for your own address and claim. Soroban verifies the proof on-chain.

    claim.tsTypeScript
    const chest = (await client.get_chest({ chest_id: chestId })).result.unwrap();// proof + nullifier come from the Loot Groth16 circuit (BN254),// generated in the browser — not exported by @findloot/sdk 0.1.1const tx = await client.claim({  chest_id: chestId,  recipient: solver, // public input of the proof  nullifier,  proof: { a, b, c },});await tx.signAndSend({ signTransaction });

Use cases

One primitive, many games.

Treasure hunts

Riddles and puzzle trails with an XLM prize at the end. The first valid proof wins.

CTF and bounty prizes

Gate a payout behind a flag. A proof copied from the mempool pays nobody else.

Campaign quests

Onboarding quests and campaigns with a fixed number of winners, set with max_claims.

API reference

Everything in @findloot/sdk.

Helpers

fingerprintAnswer
(answer: string, salt: bigint | string)
Promise<{ fingerprintHex, normalized }>
Poseidon(answer, salt) of the normalized answer. Store this on-chain, never the answer.
normalizeAnswer
(answer: string)
string
Trim, lowercase and strip accents and spaces before hashing.
answerToField
(answer: string)
bigint
Encode an answer as a BN254 field element, same as the app.
poseidon2
(a: bigint, b: bigint)
Promise<bigint>
Two-input Poseidon hash over the field.
fieldToHex
(fe: bigint | string)
string
Field element to hex, as stored in the fingerprint.

Client · @findloot/chest

createChestClient
(opts?)
Client
Soroban client for the Loot registry. Defaults to testnet and CONTRACTS.chest.
client.create_chest
({ creator, fingerprint, token, prize, max_claims, deadline })
Promise<AssembledTransaction<Result<u64>>>
Escrow a prize behind a fingerprint. Returns the chest id.
client.get_chest
({ chest_id })
Promise<AssembledTransaction<Result<Chest>>>
Read creator, token, prize, claims, max_claims, deadline, closed and fingerprint.
client.claim
({ chest_id, recipient, nullifier, proof })
Promise<AssembledTransaction<Result<void>>>
Verify the proof on Soroban, spend the nullifier, pay the recipient.
client.close_chest
({ chest_id })
Promise<AssembledTransaction<Result<i128>>>
Creator closes the chest; returns the refunded amount.

Constants

NETWORK
{ name, passphrase, rpcUrl, horizonUrl, friendbotUrl }
Stellar testnet endpoints.
CONTRACTS
{ chest, verifier, xlm }
Testnet contract ids: the registry, the Groth16 verifier and the XLM asset contract.
DEFAULT_AMOUNT_PER_SLOT_STROOPS
100000000n
10 XLM per slot, in stroops.

Proof generation (witness + Groth16 prove) is not exported in 0.1.1. Pass your wallet signer to signAndSend({ signTransaction }) (wiring unverified). client.* methods come from @findloot/chest.

Concepts

Why bots can't steal the prize.

01

Proof bound to your address

The solver address is a public input of the Groth16 proof. A copied proof cannot pay anyone else.

02

Nullifier

Every claim spends a nullifier stored on-chain, so the same proof can never be replayed.

03

Soroban verifier

The registry calls the verifier contract, which checks the BN254 proof on-chain.

04

Relayer gas

Claims can go through a relayer, so new solvers need no XLM for fees.