@elisym/merchant-node 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/cli.js +1818 -0
- package/dist/cli.js.map +1 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Igor Peregudov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# @elisym/merchant-node
|
|
2
|
+
|
|
3
|
+
The self-hosted store behind the elisym checkout. It publishes your product to Nostr, takes
|
|
4
|
+
orders sent by the checkout widget, checks each Solana payment on chain, and sends the
|
|
5
|
+
buyer what they bought.
|
|
6
|
+
|
|
7
|
+
You do not need an account or a server of ours. The node holds your store's keys and a small
|
|
8
|
+
ledger on your own disk.
|
|
9
|
+
|
|
10
|
+
## What you need
|
|
11
|
+
|
|
12
|
+
- Node.js 22.4 or newer, or Docker.
|
|
13
|
+
- A Solana wallet address to be paid to (USDC).
|
|
14
|
+
- A Solana RPC endpoint for the node itself. On mainnet, use your own provider key
|
|
15
|
+
(Helius, Triton, ...). A key restricted to a browser origin does not work from a server.
|
|
16
|
+
- What the buyer gets once paid: a link (a Blossom URL, a course page, a download) or text
|
|
17
|
+
(a license key).
|
|
18
|
+
|
|
19
|
+
## Quick start (devnet)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @elisym/merchant-node init --network devnet
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This creates `~/.elisym-merchant/` with a `config.json` to edit and the store's keys. Edit
|
|
26
|
+
`config.json`:
|
|
27
|
+
|
|
28
|
+
- `name`, `product.title`, `product.description` and `product.priceUsd`: what the store
|
|
29
|
+
sells, at what price, in USD. It is paid 1:1 in USDC.
|
|
30
|
+
- `payouts[0].address`: your Solana wallet address.
|
|
31
|
+
- `product.delivery.value`: the link or text the buyer gets.
|
|
32
|
+
|
|
33
|
+
Then publish the store and start taking orders:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx @elisym/merchant-node setup
|
|
37
|
+
npx @elisym/merchant-node run
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`setup` prints the product's `naddr`. Put it in the checkout snippet on your page:
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<elisym-buy product="naddr1..." network="devnet"></elisym-buy>
|
|
44
|
+
<script
|
|
45
|
+
src="https://pay.elisym.network/v1/embed.js"
|
|
46
|
+
integrity="<see the checkout docs>"
|
|
47
|
+
crossorigin="anonymous"
|
|
48
|
+
></script>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Keep `run` running. If it stops, a restart catches up on the orders and payments it missed.
|
|
52
|
+
Gift wraps stay on the relays for two days, and payments are read back from the chain.
|
|
53
|
+
|
|
54
|
+
## Commands
|
|
55
|
+
|
|
56
|
+
| Command | What it does |
|
|
57
|
+
| -------- | ----------------------------------------------------------------------------------- |
|
|
58
|
+
| `init` | Creates the home: a `config.json` template (never overwritten) and the store's keys |
|
|
59
|
+
| `setup` | Checks the inbox relays, publishes the store, and records the terms it offers |
|
|
60
|
+
| `run` | Takes orders, verifies payments and delivers |
|
|
61
|
+
| `orders` | Lists the orders: open, paid, delivered, and the buyer's email |
|
|
62
|
+
| `check` | Checks the inbox relays, the owner's payout list and the domain |
|
|
63
|
+
|
|
64
|
+
Every command takes `--home <dir>`. Without it, the home is `$ELISYM_MERCHANT_HOME`, else
|
|
65
|
+
`~/.elisym-merchant`.
|
|
66
|
+
|
|
67
|
+
Run `setup` again after every change to `config.json`, except `product.d`: a store sells one
|
|
68
|
+
product, and `setup` refuses a new id (the old listing would stay payable with no one taking its
|
|
69
|
+
orders). Give a new product its own home. Stop `run` first: `setup` refuses to
|
|
70
|
+
run while a node holds the home. Run `run` again afterwards. If only part of a change reaches
|
|
71
|
+
the relays (for example the payout list but not the listing), `setup` records what buyers can
|
|
72
|
+
now see, says so and fails: run it again. Each of the listing, the payout list and the inbox
|
|
73
|
+
list must also reach at least one of the relays every checkout reads
|
|
74
|
+
(`wss://relay.elisym.network`, `wss://relay.damus.io`, `wss://nos.lol`,
|
|
75
|
+
`wss://relay.nostr.band`), whatever inbox relays the store uses; `setup` fails until one
|
|
76
|
+
takes it.
|
|
77
|
+
|
|
78
|
+
The home's lock (`run.lock`) keeps two processes from writing the ledger at once, wherever they
|
|
79
|
+
run (containers and hosts sharing the home included). Its holder refreshes it every 20 seconds.
|
|
80
|
+
A node that stopped without releasing it leaves it behind, and the lock frees itself 90 seconds
|
|
81
|
+
after its last refresh. A container restarted at once may therefore fail for that long before
|
|
82
|
+
it starts.
|
|
83
|
+
|
|
84
|
+
## The config
|
|
85
|
+
|
|
86
|
+
| Field | Meaning |
|
|
87
|
+
| ------------------------- | ------------------------------------------------------------------------------- |
|
|
88
|
+
| `name` | The store's name, shown in the checkout |
|
|
89
|
+
| `nip05` | Optional. `_@your-domain.com` for level A (see below) |
|
|
90
|
+
| `network` | `devnet` or `mainnet` |
|
|
91
|
+
| `rpcUrl` | The node's Solana RPC (`https:`) |
|
|
92
|
+
| `inboxRelays` | 1 to 5 relays (`wss:`) where the store reads orders and replies |
|
|
93
|
+
| `product.d` | The product's id in the store (letters, digits, `.`, `-`, `_`) |
|
|
94
|
+
| `product.title` | Title |
|
|
95
|
+
| `product.description` | Description |
|
|
96
|
+
| `product.summary` | Optional short line |
|
|
97
|
+
| `product.priceUsd` | Price in USD, such as `"49"` or `"0.50"` |
|
|
98
|
+
| `product.delivery.method` | `access`, `download`, `license`, `api` or `webhook`: how the checkout labels it |
|
|
99
|
+
| `product.delivery.value` | The link or text the buyer gets (up to 1024 characters) |
|
|
100
|
+
| `payouts` | One `{ "caip19": ..., "address": ... }` per coin. Solana, on `network`, only |
|
|
101
|
+
|
|
102
|
+
The node refuses to start with a config it cannot use, and names every problem.
|
|
103
|
+
|
|
104
|
+
### Inbox relays
|
|
105
|
+
|
|
106
|
+
The store replies to one-time buyer keys, which have no inbox of their own. Each inbox relay
|
|
107
|
+
must therefore accept a gift wrap (kind 1059) addressed to any key and serve it back to that
|
|
108
|
+
key. It must also keep gift wraps for at least two days past their date. `setup` and `check`
|
|
109
|
+
test the first two by writing a probe and reading it back. Every relay must pass, or `setup`
|
|
110
|
+
stops: buyers send orders to all of them. Retention cannot be tested, so pick relays that keep events. `wss://nos.lol`
|
|
111
|
+
and `wss://relay.elisym.network` passed when this was written. `wss://relay.damus.io` did
|
|
112
|
+
not: it accepts gift wraps but does not serve them back.
|
|
113
|
+
|
|
114
|
+
A delivery counts as done once two inbox relays accept it (or all of them, when only one is
|
|
115
|
+
configured). While one relay is down, the node retries only that relay, each minute. An hour
|
|
116
|
+
after the payment, one relay is enough. If a relay later drops a delivery, the node sends the
|
|
117
|
+
status again when it reads that order or a receipt for it again: at most every ten minutes per
|
|
118
|
+
order, and a few at a time (the rest wait until the order is read again).
|
|
119
|
+
|
|
120
|
+
## Level A: your domain vouches for the store
|
|
121
|
+
|
|
122
|
+
Without `nip05`, the store is level C. The checkout then shows the buyer that no domain vouches
|
|
123
|
+
for it. For level A:
|
|
124
|
+
|
|
125
|
+
1. Set `"nip05": "_@your-domain.com"` and run `setup`.
|
|
126
|
+
2. Serve the `nostr.json` that `setup` writes to the home at
|
|
127
|
+
`https://your-domain.com/.well-known/nostr.json`, with the header
|
|
128
|
+
`Access-Control-Allow-Origin: *`.
|
|
129
|
+
3. Run `check`: it fetches the file and says whether the domain now vouches for the store.
|
|
130
|
+
|
|
131
|
+
Only the domain-wide name `_` gives level A. A named address such as `shop@your-domain.com`
|
|
132
|
+
stays level C.
|
|
133
|
+
|
|
134
|
+
## Docker
|
|
135
|
+
|
|
136
|
+
Build from the repository root:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
docker build -f packages/merchant-node/Dockerfile -t elisym-merchant .
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The home is the volume at `/data`. Its files belong to the image's user (uid 1000).
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
docker run --rm -v elisym-merchant:/data elisym-merchant init --network devnet
|
|
146
|
+
# edit config.json in the volume, then:
|
|
147
|
+
docker run --rm -v elisym-merchant:/data elisym-merchant setup
|
|
148
|
+
docker run -d --name shop --restart unless-stopped -v elisym-merchant:/data elisym-merchant run
|
|
149
|
+
docker logs -f shop
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
To edit the config in the volume, mount a host directory instead, for example
|
|
153
|
+
`-v "$PWD/shop:/data"`, and edit `shop/config.json`. The directory must be writable by uid 1000.
|
|
154
|
+
|
|
155
|
+
## Keeping it safe
|
|
156
|
+
|
|
157
|
+
- `keys.json` holds the store key and the owner key. Whoever has them can publish a different
|
|
158
|
+
payout address in your store's name. Keep the home private (the node creates it as `0700`)
|
|
159
|
+
and back it up. A lost key means a new store.
|
|
160
|
+
- The owner's payout list must name only payouts this node checks: Solana, on its network,
|
|
161
|
+
at the addresses its ledger records. `run` refuses to start when the published list names
|
|
162
|
+
another one, for example a Tempo address added from elsewhere or an address `setup` did not
|
|
163
|
+
record. A buyer could pay that address and never get a delivery. It refuses the same way
|
|
164
|
+
when the published listing asks a price the ledger does not record (a `setup` killed
|
|
165
|
+
between publishing and recording, for example by `docker stop`), and when the published
|
|
166
|
+
inbox list names a relay the node does not read (the config changed without `setup`). Run
|
|
167
|
+
`setup` to publish what the config names, then `run`.
|
|
168
|
+
- The node never sends email. It records the buyer's email when the checkout asked for one
|
|
169
|
+
(`orders` lists it), and sending anything is up to you.
|
|
170
|
+
- `ledger.json` records which transaction paid which order. Do not edit it or restore an older
|
|
171
|
+
copy while orders are open: a payment could then be credited twice.
|
|
172
|
+
|
|
173
|
+
## Limits
|
|
174
|
+
|
|
175
|
+
- One product per node, paid on Solana (USDC) on one network.
|
|
176
|
+
- Delivery is the configured link or text. Uploading a file to Blossom is up to you; the link
|
|
177
|
+
goes in `product.delivery.value`.
|
|
178
|
+
- Refunds are made by hand from your wallet.
|