Logos Kit

Worked example: faucet

Test funds into the account the user picks, public or private, with rate limits handled.

Logos Kit Faucet (modules/logos_kit_faucet) gets test LGO into any account the user shares. It gives each faucet outcome its own state, including the ones where it can't tell yet, and the private path, where funds arrive with a proof.

The faucet app: picking a public or private account
The faucet app: any shared account, public or private.

On the official testnet the wallet uses Logos Kit's drip faucet: 1 LGO per request, each account once an hour, each IP five times an hour (Networks).

1. Connect, both kinds

kit.api.connect({ accountKinds: ["public", "private"] })

Private accounts need their own consent in the wallet's connect sheet. The app gets a per-app handle for each (pvt_…); it never learns the account itself.

2. Pick, then request

kit.api.requestFunds(account).then(function (r) {
    if (r.status === "funded")           r.shieldHandle ? follow(r.shieldHandle) : done(r.amount)
    else if (r.status === "rate_limited") countdown(r.retryAfterSeconds)
    else if (r.status === "outcome_unknown") watchBalance(r.fundedAccount || account)
    else                                  declined(r.reason)
})
OutcomeWhat the app shows
fundedthe amount (lepta: show kit.sdk.formatUnits(r.amount, 9) LGO), the account, a link to the transaction
rate_limiteda countdown on the button, which reopens at zero
outcome_unknown"Checking…": it watches the balance instead of asking again (a second request could pay twice)
rejectedthe faucet's reason

When the outcome is unknown

The faucet answered ambiguously (a timeout, a dropped connection), so the funds may or may not have landed. The app never re-requests on its own. Instead it:

  1. Re-reads the paid account's balance on a timer, for a bounded time. Each attempt counts, including failed reads, so an unreachable node can't keep it checking forever.
  2. Shows Funds arrived as soon as the balance goes up.
  3. Otherwise ends in an unresolved state: "We couldn't confirm the payment", with Check again (one more balance read) and a plain note not to request again until the check settles.

For a private target, the account to watch is not the private one. The faucet only pays public accounts, so it paid r.fundedAccount, one of the user's public accounts. The app watches that account and says so ("The faucet paid Public account 1"). If the funds turn up there, the user can move them into the private account from the wallet.

Rate limited, with a countdown
Rate limited, with a countdown.
Outcome not confirmed yet, with a safe re-check
Not confirmed yet: a safe re-check, never a second claim.

3. The private path

The faucet only pays public accounts. For a private target the wallet funds one of the user's public accounts (fundedAccount), then queues a transfer into the private account (a shield), which the user approves in the wallet. The app follows it with the returned shieldHandle:

kit.api.watchTransaction(r.shieldHandle, function (s) {
    // awaiting_approval → proving (minutes, on the user's machine) → submitted → included
    if (s.lifecycle !== "included") return
    if (s.outcome === "success")      done()           // the private balance moved
    else if (s.outcome === "failure") failed(s)       // funds are still on fundedAccount
    else                              unconfirmed(s)   // included, outcome unconfirmed
})

The app shows four steps (paid, approve, proving with a timer, included). Only outcome === "success" shows Done. An included shield with outcome unknown gets its own Included, outcome unconfirmed state, which says the funds are either in the private account or still on the public one, and offers a balance check. If the user declines the move, the app says plainly that the funds stay on the public account.

Moving test funds into a private account: proving
Moving the funds into the private account: proving.
Test funds arrived in a private account
Arrived, privately.

Run it yourself

e2e/faucet-app.sh   # public claim, rate limit, private claim through a shield

On this page