Logos Kit

Worked example: testimonials

The testimonial app, step by step: connect, account selection, approval, transaction.

Logos Kit Testimonials (modules/logos_kit_testimonial) posts a short testimonial to a LEZ program that stores it on chain, and shows what others wrote. It's the SDK's reference integration: every step of connect → account selection → approval → transaction, with the states around it.

The testimonial app in Basecamp after a post landed
Logos Kit Testimonials in Basecamp, after a post landed.

The program

A standard LEZ program (programs/testimonial) keeps one record per (submission, author) and paged stats with monthly counts. A post carries the text (≤ 280 bytes), an optional username (≤ 32 bytes) and the submission id LP-0021/logos-kit, and the chain checks its timestamp against the block time. Each account posts once. The author signs and pays the fee, so a post is always public.

On the official testnet it's deployed as an immutable program, 5YoH3xjhgeKt2mcJXW7c31bqDNCWWA4CRJxVdvzFvVef (TESTIMONIAL_PROGRAMS["lez:testnet"]). Posts there count for LP-0021 adoption; posts on the preview network don't.

1. Connect: the user picks the account

kit.api.connect({ accountKinds: ["public"] }).then(useSession, function (e) {
    root.note = kit.sdk.isUserRejection(e) ? "" : errText(e)
})

The shell routes lez.wallet.connect to the wallet, which asks which account to share. The app only ever sees accounts the user picked. On start, the app calls getSession() to restore an earlier connection without a prompt.

2. Read before asking

Before showing the composer, the app reads three things through the wallet:

kit.api.getWalletBalance(account)             // enough for the fee?
kit.api.readAccount(account, programId)       // nonce: has it been used?
kit.api.getTestimonial(programId, account)    // already posted?

That decides the state: already posted (show it), no funds (offer test funds in place: kit.api.requestFunds(account)), a fresh account (a gentle nudge: posts from accounts with real use count for more), or the composer.

A fresh account with no funds, offered test funds in-flow
A fresh account gets test funds without leaving the app.

3. Compose, and validate like the chain does

The app checks what the program would refuse, before the user approves anything: byte length (not characters), blank text, control and direction-override characters. It also requires the text to name the wallet, since testimonials count for the wallet they name.

4. Propose: the user approves in the wallet

kit.api.postTestimonial({ program: programId, author: account, text: text, username: name })
    .then(function (r) {
        root.watcher = kit.api.watchTransaction(r.handle, onStatus)
    })

postTestimonial finds the open stats page and builds the call. The wallet shows the approval: the program (with its source-verification and upgrade status), the fee, and the decoded post. The promise resolves once the user approves, with a handle; inclusion comes after.

Approving a testimonial post: decoded, source-verified, with the fee cap
What the user approves: the post, decoded from the exact message, with the fee cap.

5. Follow the transaction

function onStatus(s) {
    if (s.lifecycle === "included" || s.lifecycle === "finalized") {
        if (s.outcome === "success") done()
        else if (s.outcome === "failure") recheckAfterReject(s)
        else confirm()                       // included, outcome unknown: the record decides
    } else if (s.lifecycle === "rejected" || s.lifecycle === "dropped" || s.lifecycle === "expired") {
        recheckAfterReject(s)
    }
}

// Unknown outcome: read the author's record. Our post there means it landed.
function confirm() {
    kit.api.getTestimonial(programId, account).then(function (t) {
        root.phase = t && t.text === root.text ? "done" : "unconfirmed"
    }, function () { root.phase = "unconfirmed" })
}

Only success, or the record itself, shows Done. Otherwise the app shows Not confirmed yet with a re-check, never a second post. A rejected post can still mean the account already posted (another device), so the app reads the record again before calling it a failure. When it's done, the app shows the transaction and block, and kit.api.openExplorer({ txHash }) opens it in the user's browser (the QML sandbox can't open links; the wallet builds the URL).

A testimonial post on its way to a block
On its way to a block.
A testimonial post included and confirmed from its on-chain record
Included, and confirmed from the on-chain record.

The feed

kit.api.getTestimonials(programId, { limit: 25 })   // { count, monthly, latest }

Everything comes from chain data: the count, this month's tally and the newest posts, decoded from the program's accounts.

Run it yourself

just e2e-testimonial-app   # local sequencer, program deploy, the whole flow in the dev harness

On this page