@nano133/late-returns
v0.4.1
Published
Send Nano (XNO) payments that nothing claimed back to their sender, exactly once.
Maintainers
Readme
@nano133/late-returns
Stage: Beta. What this means: https://nano133.com/stages
Send Nano (XNO) payments that nothing claimed back to their sender, exactly once.
[!WARNING] This package sends real money from a wallet whose key is on your server. A key on a server is a hot wallet: anyone who gets into the server can empty it. Keep only small amounts in it (what your late payments and refunds need, nothing more), keep the key in your secret manager (never in code or in the database), and use your own Nano node. Test everything against a mock ledger or with tiny amounts first. The software comes with no warranty (MIT license).
What it does
A site that takes Nano gives each checkout an address of its own (or a unique amount) and matches the payment that arrives. Some payments match nothing: they arrive after the checkout closed, or someone sends the wrong amount. Without this package that money stays in your wallet, and the buyer has to ask for it back.
With it, a run every few minutes finds such payments and sends each one back to its sender:
| Rule | Default |
|---|---|
| Only payments of at least | 0.001 XNO (minRaw) |
| Only payments first seen at least | 2 hours ago (minAgeSec) |
| Only confirmed sends to the wallet | always |
| Never a payment your site already used | store.isUsed() |
| Never a payment an open checkout can still claim | your isClaimable() |
| Never a payment from these senders | never (your own wallets, an admin's top-ups) |
| At most | 3 new returns per run (perRun), 30 per day (perDay) |
| At most, per sender | no limit (perSenderPerDay, off by default) |
Each return is recorded together with a "used" mark for the payment, so a late claim can't also take it. The wallet then receives exactly that payment and sends exactly its amount back.
Exactly once
Every send is recorded before it is published: its hash, the frontier it builds on, and the signed block. A later run checks the ledger first: if the block is there, the return is done; if not and the wallet hasn't changed, the same block is published again (it can only land once); a new block is made only when the old one can never be valid. All writes happen under a lock that each write proves it still holds. This is the code nano133.com runs for its till, extracted.
Install
npm install @nano133/late-returnsNo runtime dependencies. You bring four things:
| You provide | What it is |
|---|---|
| rpc | A function that calls your Nano node and rejects on { error }. |
| signer | Signs blocks for the wallet. See examples/signer.ts (nanocurrency-web). The package never sees the key. |
| store | Where returns are kept: memoryStore() (tests) or firestoreStore(db, …), or your own ReturnStore. |
| isClaimable | Your answer to "can one of my open checkouts still claim this amount?" |
Use
import { runLateReturns } from "@nano133/late-returns";
import { firestoreStore } from "@nano133/late-returns/firestore";
const store = firestoreStore(db, {
returns: "late_returns", // the return records
used: ["payments"], // where your checkouts record a claimed payment (by block hash)
meta: "late_returns_meta", // the lock and the daily counters
});
// Every few minutes (a cron job, or a page load with your own throttle):
await runLateReturns({
rpc, // your node
store,
signer, // from your secret manager
never: [OWNER_ADDRESS], // top-ups from you never go back
isClaimable: async ({ amount }) =>
(await db.collection("checkouts").where("amount", "==", amount).where("status", "==", "waiting").get())
.docs.some((d) => d.data().expiresAt + GRACE_MS > Date.now()),
});Your claim code must refuse a payment whose hash is already in a used
collection; then a payment that went back can never also pay a checkout.
A limit per sender
Someone can flood your wallet with small wrong payments, and each one costs
you a return (two blocks). Set perSenderPerDay to stop that:
await runLateReturns({ /* … */, perSenderPerDay: 10 });A sender's first 10 late payments in a UTC day go back. From the 11th that
day, a payment is kept: its record has status: "kept" and
reason: "limit", it is marked used (no checkout can claim it), and it is
never sent. Kept payments don't count toward perDay, so one sender can't
use up the day's returns for everyone else. Say so in your terms.
To send a kept payment back after all (a real customer who made mistakes):
import { releaseKept } from "@nano133/late-returns";
await releaseKept({ rpc, store }, blockHash, "[email protected]"); // "released" | "not-kept" | "missing"It goes back with the next run, exactly once. firestoreStore keeps each
sender's daily count in a document sender-<day>-<hash of the address> with a
deleteAt date: add a TTL policy on deleteAt for that collection.
More than one receiving address
By default the package watches one address, the signer's. A site that gives each checkout its own derived address can give the package those addresses and a signer for each:
await runLateReturns({
/* … */
signer, // the main wallet, scanned first
addresses: async () => await listSweepAddresses(), // or a plain array
signerFor: (address) => signerForDerivedAddress(address), // null: this return waits
});Each run scans receivable on the wallet's own address, then on every address
in addresses (one call each; an extra address that fails to answer is
skipped for that run). A payment is received and returned by the signer of the
address that got it, which the package reads from your node. isClaimable
also gets to, the address that received the payment.
With @nano133/checkout 0.3.0, ...derivedReturns({ store, secret })
(from @nano133/checkout/derived) gives addresses, signerFor and an
isClaimable that answers by the address: only the exact quote of a waiting
checkout at its own address stays, and any other amount goes back in full. Use
the same payments collection for the checkout store and for used.
The limits count across all the addresses together: perRun, perDay and
perSenderPerDay are totals, not per address. A run that reaches perRun
stops scanning, so the addresses at the end of the list wait for a later run.
Every address you list is also treated as a never sender.
Your own database
Implement ReturnStore (see src/types.ts). The one hard rule: record()
must be atomic (create the return, mark the payment used, count the day, or
do none of it), and every update() made under a lock must fail with
LockLost when the lock is gone.
Cost
Each return is two blocks for your wallet: one receive and one send. Each
needs proof of work, which work_generate on your node computes (or pass
your own work). Nano has no fees. The caps keep a flood of small payments
from turning into work: someone who sends you 0.001 XNO gets it back, and
costs your node two blocks of work, at most 30 times a day.
Test
npm test # the mock ledger
FIRESTORE_EMULATOR_HOST=localhost:8080 npm test # also the Firestore store, on an emulator
npm run example # a walk-through on the mock ledgerLicense
MIT. From nano133.com.
