@geeqdev/geeq-sdk 0.0.0-stage → 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/README.md +593 -2
- package/package.json +70 -4
package/README.md
CHANGED
|
@@ -1,3 +1,594 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @geeqdev/geeq-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Add Geeq to a web app without learning the Geeq protocol.
|
|
4
|
+
|
|
5
|
+
The SDK creates wallets, reads the chain, picks a node, builds and signs transactions,
|
|
6
|
+
and tells you whether the chain accepted them. It runs in the browser and in Node.
|
|
7
|
+
|
|
8
|
+
> **Version 0.0.1 (early).** Supports wallets, attestations, coin transfers and NFTs.
|
|
9
|
+
> The SDK **signs** transactions, but **your app sends them** (one `fetch` call, shown below).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Contents
|
|
14
|
+
|
|
15
|
+
- [Install](#install)
|
|
16
|
+
- [Quick start](#quick-start)
|
|
17
|
+
- [Key ideas](#key-ideas)
|
|
18
|
+
- [Wallets](#wallets)
|
|
19
|
+
- [Connecting to a chain](#connecting-to-a-chain)
|
|
20
|
+
- [Transactions](#transactions)
|
|
21
|
+
- [Sending and tracking](#sending-and-tracking)
|
|
22
|
+
- [Amounts and fees](#amounts-and-fees)
|
|
23
|
+
- [Rules to know](#rules-to-know)
|
|
24
|
+
- [Errors](#errors)
|
|
25
|
+
- [Setup notes](#setup-notes)
|
|
26
|
+
- [Reference](#reference)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @geeqdev/geeq-sdk
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Needs Node 20+ or any browser with WebAssembly. Nothing else to install.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import {
|
|
44
|
+
initChain, keypairFromMnemonic, attest, GeeqSocket, submitRoute, attestationSubmitBody,
|
|
45
|
+
} from "@geeqdev/geeq-sdk";
|
|
46
|
+
|
|
47
|
+
// 1. Connect to a chain and open a socket to its node. Do this once, at startup.
|
|
48
|
+
const chain = await initChain({
|
|
49
|
+
discovery: simulatorUrl, // the simulator for your environment
|
|
50
|
+
chainName: "test", // which of its chains to use
|
|
51
|
+
penniesPerGeeq, // your app's setting, see "Amounts and fees"
|
|
52
|
+
});
|
|
53
|
+
const socket = await GeeqSocket.connect({ endpoint: chain.endpoint });
|
|
54
|
+
|
|
55
|
+
// 2. Load the user's wallet from their recovery phrase.
|
|
56
|
+
const keypair = await keypairFromMnemonic(phrase);
|
|
57
|
+
|
|
58
|
+
// 3. Build and sign a transaction. Here: attest a file.
|
|
59
|
+
const tx = await attest({
|
|
60
|
+
keypair,
|
|
61
|
+
content: fileInput.files[0], // a File, string or bytes
|
|
62
|
+
metadata: ["invoice-2026-09"], // optional public labels
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// 4. Start listening, THEN send it.
|
|
66
|
+
const tracked = await socket.track(tx);
|
|
67
|
+
const response = await fetch(submitRoute(tx.nodeUrl), {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "content-type": "application/json" },
|
|
70
|
+
body: attestationSubmitBody(tx.transaction, tx.payload),
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) tracked.refused({ status: response.status });
|
|
73
|
+
|
|
74
|
+
// 5. Wait for the result (about one block).
|
|
75
|
+
const outcome = await tracked.settled(); // status: "confirmed" | "rejected" | "unknown"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Every other transaction follows the same five steps.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Key ideas
|
|
83
|
+
|
|
84
|
+
| Term | Meaning |
|
|
85
|
+
|---|---|
|
|
86
|
+
| **Recovery phrase** | 12–24 English words. This *is* the wallet: whoever has it owns the account. |
|
|
87
|
+
| **Keypair** | Made from the phrase. It signs transactions. |
|
|
88
|
+
| **Account id** | A 32-byte hash of the public key. The chain uses it to identify the account. |
|
|
89
|
+
| **Address** | A string form of the account, for one specific chain. |
|
|
90
|
+
| **Node** | The server you talk to. The SDK picks one for you. |
|
|
91
|
+
| **Nonce** | The account's transaction counter. It stops a transaction from being replayed. |
|
|
92
|
+
| **Pennies / Geeq** | The chain counts pennies. "Geeq" is a display unit your app defines. |
|
|
93
|
+
|
|
94
|
+
You never supply the chain number, node number or nonce yourself. The SDK reads them from
|
|
95
|
+
the chain.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Wallets
|
|
100
|
+
|
|
101
|
+
### Recovery phrases
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { generateMnemonic, validateMnemonic, normalizeMnemonic } from "@geeqdev/geeq-sdk";
|
|
105
|
+
|
|
106
|
+
const phrase = generateMnemonic(); // 12 words
|
|
107
|
+
generateMnemonic({ words: 24 }); // 12, 15, 18, 21 or 24
|
|
108
|
+
validateMnemonic(phrase); // true
|
|
109
|
+
normalizeMnemonic(pastedText); // tidies spacing and case
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
These are instant and work before the WebAssembly engine has loaded.
|
|
113
|
+
|
|
114
|
+
### Keypairs
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import {
|
|
118
|
+
keypairFromMnemonic, generateKeypair, importKeypair, keypairFromSecretKey,
|
|
119
|
+
} from "@geeqdev/geeq-sdk";
|
|
120
|
+
|
|
121
|
+
const keypair = await keypairFromMnemonic(phrase);
|
|
122
|
+
// Also: generateKeypair(), importKeypair(publicKey, privateKey), keypairFromSecretKey(privateKey)
|
|
123
|
+
|
|
124
|
+
keypair.publicKey // 32 bytes
|
|
125
|
+
keypair.privateKey // 32 bytes, keep secret
|
|
126
|
+
keypair.accountId // 32 bytes
|
|
127
|
+
keypair.getAddress({ chainNumber: 1 }) // wallet address
|
|
128
|
+
keypair.sign(bytes)
|
|
129
|
+
keypair.dispose() // call this when you are done with the keypair
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`keypairFromMnemonic` takes options. Each one changes the key you get:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
await keypairFromMnemonic(phrase, {
|
|
136
|
+
passphrase: "", // optional extra password
|
|
137
|
+
coinType: "mainnet", // or "testnet"
|
|
138
|
+
recordType: "coin-asset",
|
|
139
|
+
account: 0,
|
|
140
|
+
addressIndex: 0,
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
- **Call `dispose()`** when you replace or drop a keypair. It frees memory in the engine.
|
|
145
|
+
After that, `sign()` throws.
|
|
146
|
+
- **In Vue or React state**, don't make it deeply reactive. Use `shallowRef` + `markRaw` in
|
|
147
|
+
Vue, or keep it out of `useState` in React.
|
|
148
|
+
- `JSON.stringify(keypair)` only outputs the public key, so the private key can't leak into
|
|
149
|
+
logs.
|
|
150
|
+
|
|
151
|
+
### Addresses
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { createWalletAddress, decodeWalletAddress, isValidWalletAddress } from "@geeqdev/geeq-sdk";
|
|
155
|
+
|
|
156
|
+
const address = keypair.getAddress({ chainNumber: 1 });
|
|
157
|
+
await createWalletAddress({ publicKey, chainNumber: 1 }); // same, without a keypair
|
|
158
|
+
await decodeWalletAddress(address); // { accountId, chainNumber, recordType }
|
|
159
|
+
isValidWalletAddress(address); // true
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**An address only works on one chain.** The chain number is part of the address, so the
|
|
163
|
+
same key gives different addresses on different chains.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Connecting to a chain
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { initChain, GeeqChain } from "@geeqdev/geeq-sdk";
|
|
171
|
+
|
|
172
|
+
// Set a default chain for the whole app (used by attest() and transfer()):
|
|
173
|
+
const chain = await initChain({ discovery: simulatorUrl, chainName: "test", penniesPerGeeq });
|
|
174
|
+
|
|
175
|
+
// Or create a separate instance (for tests, multiple chains, server rendering):
|
|
176
|
+
const other = await GeeqChain.connect({ discovery: simulatorUrl, chainName: "test", penniesPerGeeq });
|
|
177
|
+
|
|
178
|
+
// Or skip discovery if you already know a node:
|
|
179
|
+
await GeeqChain.connect({ endpoint: "https://1.2.3.4:2082", penniesPerGeeq });
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
With `discovery`, the SDK asks the simulator which nodes are running, picks one at random
|
|
183
|
+
and uses it for everything. You can name the chain by `chainName` or `chainNumber`.
|
|
184
|
+
This works for every chain and environment: pass the simulator URL of the one you use.
|
|
185
|
+
|
|
186
|
+
- Nodes use **port 2082**. If you leave the port out, the SDK adds it.
|
|
187
|
+
- `connect` doesn't read the chain yet. That happens when you build your first transaction,
|
|
188
|
+
so `chain.node` is `undefined` until then.
|
|
189
|
+
|
|
190
|
+
To see the account's current state (balance, nonce, limits):
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
const state = await chain.getChainState({ account: keypair });
|
|
194
|
+
chain.formatGeeq(state.balance); // e.g. "12.5"
|
|
195
|
+
state.minimumTransfer; // smallest allowed transfer, in pennies
|
|
196
|
+
state.metadataLimits; // how many labels, and how long each can be
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Transactions
|
|
202
|
+
|
|
203
|
+
Each method below reads the chain, checks your input, signs, and returns a `tx`. **It
|
|
204
|
+
doesn't send anything.** See [Sending and tracking](#sending-and-tracking).
|
|
205
|
+
|
|
206
|
+
| What | Method | Send with |
|
|
207
|
+
|---|---|---|
|
|
208
|
+
| Attest a file or text | `chain.createAttestation` (or `attest`) | `attestationSubmitBody` |
|
|
209
|
+
| Send coin or NFTs | `chain.createTransfer` (or `transfer`) | `coinTransferSubmitBody` |
|
|
210
|
+
| Create an NFT mint proxy | `chain.createNftMintProxy` | `coinTransferSubmitBody` |
|
|
211
|
+
| Mint an NFT | `chain.mintNft` | `attestationSubmitBody` |
|
|
212
|
+
|
|
213
|
+
Every `tx` includes:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
tx.transaction // base64, the signed transaction
|
|
217
|
+
tx.payload // base64, attestations and mints only
|
|
218
|
+
tx.nodeUrl // the node you must send it to
|
|
219
|
+
tx.fee // what the chain will charge (see "Amounts and fees")
|
|
220
|
+
tx.transactionHash // 32 bytes, the transaction's id
|
|
221
|
+
tx.chainState // the chain values it was signed with
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Attest a file
|
|
225
|
+
|
|
226
|
+
Puts the file's hash on the chain. **The file itself never leaves the device.**
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const tx = await chain.createAttestation({
|
|
230
|
+
keypair,
|
|
231
|
+
content: file, // File, Blob, string, Uint8Array or ArrayBuffer
|
|
232
|
+
metadata: ["invoice-2026-09"], // optional
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
If you already have a 32-byte hash, pass `attestHash` instead of `content`.
|
|
237
|
+
|
|
238
|
+
**Metadata labels are public.** Use them for ids, not secrets. Each chain limits how many
|
|
239
|
+
labels you can add and how long each one is (in UTF-8 bytes, so one emoji counts as 4). To
|
|
240
|
+
check labels while the user types:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
const report = inspectMetadata(labels, state.metadataLimits);
|
|
244
|
+
report.withinLimits; // false if any label is too long, or there are too many
|
|
245
|
+
report.entries[0]; // { index: 0, bytes: 40, overLimit: true }
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Send coin
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
const tx = await chain.createTransfer({
|
|
252
|
+
from: keypair,
|
|
253
|
+
to: recipientAddress,
|
|
254
|
+
amount: { geeq: "1.50" },
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Before signing, the SDK checks that:
|
|
259
|
+
|
|
260
|
+
- the address belongs to this chain (`chain-mismatch`),
|
|
261
|
+
- the amount is at least the chain's minimum (`amount-below-minimum`),
|
|
262
|
+
- the sender can pay the amount plus the fee (`insufficient-funds`).
|
|
263
|
+
|
|
264
|
+
### NFTs
|
|
265
|
+
|
|
266
|
+
NFTs are minted through a **proxy**: a separate account that holds coin to pay for mints.
|
|
267
|
+
The wallet that creates it becomes its **admin**, and only the admin can mint with it.
|
|
268
|
+
|
|
269
|
+
**1. Create a proxy** (once):
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
const tx = await chain.createNftMintProxy({
|
|
273
|
+
admin: keypair,
|
|
274
|
+
amount: { geeq: "5" }, // coin moved onto the proxy to pay for mints
|
|
275
|
+
});
|
|
276
|
+
tx.index; // 1 for the admin's first proxy, then 2, 3…
|
|
277
|
+
tx.proxyAddress; // send coin here to top it up later
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**2. Find the admin's proxies** (no storage needed, they are derived from the key):
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
const { proxies, nextIndex } = await chain.findNftMintProxies({ admin: keypair, maxAgeMs: 0 });
|
|
284
|
+
// proxies: [{ index: 1, address, balance, … }]
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**3. Mint an NFT:**
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
const tx = await chain.mintNft({
|
|
291
|
+
admin: keypair,
|
|
292
|
+
index: 1, // which proxy to mint with
|
|
293
|
+
content: file, // hashed locally, never sent
|
|
294
|
+
metadata: ["deed", "lot 7"], // optional public labels
|
|
295
|
+
// to: someAddress, // optional, defaults to the admin
|
|
296
|
+
});
|
|
297
|
+
tx.nftId; // the new NFT's id
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The proxy pays the fee, not the admin. The same content and labels can only be minted once
|
|
301
|
+
per proxy.
|
|
302
|
+
|
|
303
|
+
**4. List and send NFTs:**
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
const nfts = await chain.findNfts({ owner: keypair, maxAgeMs: 0 });
|
|
307
|
+
// [{ id, attestHash, metadata, counterparty }]
|
|
308
|
+
|
|
309
|
+
const tx = await chain.createTransfer({
|
|
310
|
+
from: keypair,
|
|
311
|
+
to: recipientAddress,
|
|
312
|
+
nfts: [nfts[0].id], // one or more
|
|
313
|
+
// amount is optional, defaults to the chain's minimum transfer
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
New proxies and NFTs are listed only after a block confirms them. Pass `maxAgeMs: 0` to
|
|
318
|
+
skip the cache when you check right after sending.
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Sending and tracking
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import { GeeqSocket, submitRoute, attestationSubmitBody } from "@geeqdev/geeq-sdk";
|
|
326
|
+
|
|
327
|
+
const socket = await GeeqSocket.connect({ endpoint: chain.endpoint }); // once per app
|
|
328
|
+
|
|
329
|
+
const tracked = await socket.track(tx); // 1. listen first
|
|
330
|
+
const response = await fetch(submitRoute(tx.nodeUrl), { // 2. then send
|
|
331
|
+
method: "POST",
|
|
332
|
+
headers: { "content-type": "application/json" },
|
|
333
|
+
body: attestationSubmitBody(tx.transaction, tx.payload),
|
|
334
|
+
// for transfers and proxies: coinTransferSubmitBody(tx.transaction)
|
|
335
|
+
});
|
|
336
|
+
if (!response.ok) {
|
|
337
|
+
tracked.refused({ status: response.status, message: await response.text() });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const outcome = await tracked.settled(); // 3. wait for the verdict
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Always call `track` before you send.** Otherwise you can miss the result. If `track`
|
|
344
|
+
throws, don't send.
|
|
345
|
+
|
|
346
|
+
**A `200` response is not a confirmation.** It only means the node received a valid
|
|
347
|
+
transaction. Wait for `settled()` before you show success.
|
|
348
|
+
|
|
349
|
+
`settled()` never throws. It resolves to one of:
|
|
350
|
+
|
|
351
|
+
| `status` | `reason` | Meaning |
|
|
352
|
+
|---|---|---|
|
|
353
|
+
| `confirmed` | | Done. It's in a block. |
|
|
354
|
+
| `rejected` | `invalid` | Failed when the block was built (usually nonce or balance). **The fee is still charged.** |
|
|
355
|
+
| `rejected` | `nonsane` | The node refused it. |
|
|
356
|
+
| `rejected` | `refused` | Your POST failed (you reported it with `tracked.refused`). |
|
|
357
|
+
| `rejected` | `incorrectly-included` | A dishonest node included it. Not your fault. |
|
|
358
|
+
| `unknown` | `timeout` | No answer in 5 minutes. **It may still have succeeded**: check the account's nonce before retrying. |
|
|
359
|
+
| `unknown` | `closed`, `released` | The socket was closed, or you called `tracked.release()`. |
|
|
360
|
+
|
|
361
|
+
Show `outcome.message` to users, but write your logic against `reason`. For a progress
|
|
362
|
+
indicator, use `tracked.onChange(t => …)`.
|
|
363
|
+
|
|
364
|
+
The socket reconnects on its own. Keep **one socket per chain** for the life of the page
|
|
365
|
+
and call `socket.dispose()` when you're done.
|
|
366
|
+
|
|
367
|
+
### Other socket events
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
await socket.subscribe([keypair]); // get events for these accounts
|
|
371
|
+
socket.onTxStatus((event) => { /* any tx status, including incoming coin */ });
|
|
372
|
+
socket.on("coin-account-update", (payload) => { /* … */ });
|
|
373
|
+
socket.onConnectionChange((state) => setOffline(state !== "open"));
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Incoming transfers arrive through `onTxStatus` with `event.direction === "incoming"`.
|
|
377
|
+
Use `event.id` to remove duplicates.
|
|
378
|
+
|
|
379
|
+
### Looking up a transaction later
|
|
380
|
+
|
|
381
|
+
After a page reload the socket can't tell you what happened. Ask a block explorer instead:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
import { getExplorerTransaction, transactionIdOf } from "@geeqdev/geeq-sdk";
|
|
385
|
+
|
|
386
|
+
const found = await getExplorerTransaction({
|
|
387
|
+
explorerUrl: "https://blkexp.geeq-test.io",
|
|
388
|
+
refHash: transactionIdOf(tx), // save this before the reload
|
|
389
|
+
});
|
|
390
|
+
// null = the explorer doesn't have it yet (or it was never included)
|
|
391
|
+
// otherwise: { status, type, block, from, to, values, … }
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Amounts and fees
|
|
397
|
+
|
|
398
|
+
The chain only counts **pennies**. How many pennies make one **Geeq** is up to your app.
|
|
399
|
+
You set it once, as `penniesPerGeeq`, when you connect. It must be a bigint power of ten
|
|
400
|
+
(`1n`, `10n`, `100n`, …). **Use the same value as your wallet and explorer**
|
|
401
|
+
(`VUE_APP_BALANCE_DIVISOR` in Geeq's apps), or the numbers won't match.
|
|
402
|
+
|
|
403
|
+
Every amount must say its unit. A bare number like `1.5` is rejected, so a mix-up can't
|
|
404
|
+
send the wrong amount.
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
{ geeq: "12.34" } // what a person types
|
|
408
|
+
{ pennies: 1234n } // what the chain counts
|
|
409
|
+
|
|
410
|
+
chain.toPennies({ geeq: "1.5" }); // → bigint
|
|
411
|
+
chain.formatGeeq(pennies); // → "1.5"
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
If an amount has more decimal places than your unit allows, it is rejected, not rounded.
|
|
415
|
+
|
|
416
|
+
**Fees.** `tx.fee` is the exact fee, in pennies, paid on top of the amount:
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
chain.formatGeeq(tx.fee.total); // data + coin
|
|
420
|
+
tx.fee.data; // charged per byte of the transaction
|
|
421
|
+
tx.fee.coin; // 0.25% of the amount sent (capped)
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
The sender pays the fee. For a mint, the proxy pays it.
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
## Rules to know
|
|
429
|
+
|
|
430
|
+
These cause most problems:
|
|
431
|
+
|
|
432
|
+
1. **Only one transaction per account per block.** Two transactions with the same nonce
|
|
433
|
+
are *both* rejected. The SDK blocks a second build with `nonce-collision`. If the first
|
|
434
|
+
one was never sent, give its nonce back and build again:
|
|
435
|
+
```ts
|
|
436
|
+
chain.discard(tx); // only for a tx you did NOT send
|
|
437
|
+
```
|
|
438
|
+
For mints, the rule applies to the proxy instead of the admin.
|
|
439
|
+
|
|
440
|
+
2. **Send within about 9 blocks.** After that the node rejects the transaction. If the user
|
|
441
|
+
waited on a confirm screen, check first:
|
|
442
|
+
```ts
|
|
443
|
+
if (await chain.isExpired(tx)) { chain.discard(tx); /* build again */ }
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
3. **Always send to `tx.nodeUrl`.** The transaction names its node. Any other node rejects it.
|
|
447
|
+
|
|
448
|
+
4. **New accounts don't exist yet.** A new wallet isn't on the chain until it receives coin,
|
|
449
|
+
so you'll get `account-not-found`. Show a "fund your account" message, not an error.
|
|
450
|
+
|
|
451
|
+
5. **Browser network errors all look the same.** CORS, an untrusted certificate, mixed
|
|
452
|
+
content (an https page calling an http node) and a dead node all show up as
|
|
453
|
+
`network-unreachable`. The error message lists the possible causes. Nodes use bare IP
|
|
454
|
+
addresses, so the browser must trust their certificates.
|
|
455
|
+
|
|
456
|
+
6. **"Processing" doesn't mean the node is running.** A crashed node can still be listed as
|
|
457
|
+
processing. The only way to know is to call it.
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## Errors
|
|
462
|
+
|
|
463
|
+
Every error is a `GeeqError` with a stable `code`. **Check `code`, not the message.**
|
|
464
|
+
|
|
465
|
+
```ts
|
|
466
|
+
try {
|
|
467
|
+
await attest({ keypair, content: file });
|
|
468
|
+
} catch (error) {
|
|
469
|
+
if (error instanceof GeeqError && error.code === "account-not-found") {
|
|
470
|
+
// ask the user to fund the account
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
| Code | Meaning | What to do |
|
|
476
|
+
|---|---|---|
|
|
477
|
+
| `not-initialized` | Engine or default chain not ready | Await `initGeeq()` / `initChain()` first |
|
|
478
|
+
| `init-failed` | Engine failed to load | Check your bundler setup (see [Setup notes](#setup-notes)) |
|
|
479
|
+
| `invalid-key` | Wrong key length, or keys don't match | |
|
|
480
|
+
| `invalid-address` | Bad wallet address | |
|
|
481
|
+
| `invalid-argument` | A value is out of range | |
|
|
482
|
+
| `network-unreachable` | No response from the server | See rule 5 above |
|
|
483
|
+
| `node-error` | Server returned an error | Check `error.status` and `error.nodeDetails` |
|
|
484
|
+
| `ledger-unreadable` | Server replied, but not with chain data | Check the URL |
|
|
485
|
+
| `chain-mismatch` | Wrong chain (node or address) | Use the right chain |
|
|
486
|
+
| `account-not-found` | Account has never received coin | Fund it |
|
|
487
|
+
| `account-not-attestable` | Account type can't sign this | Use a `coin-asset` key |
|
|
488
|
+
| `node-unusable` | Node can't be identified or won't take transactions | Use another node; the message lists some |
|
|
489
|
+
| `metadata-too-large` | Labels too long or too many | Shorten them; the message says which one |
|
|
490
|
+
| `nonce-collision` | Second transaction in the same block | See rule 1 above |
|
|
491
|
+
| `insufficient-funds` | Can't pay amount + fee | Send less, or add funds |
|
|
492
|
+
| `amount-below-minimum` | Amount below the chain's minimum | Send more |
|
|
493
|
+
| `nft-not-owned` | Sender doesn't hold that NFT | Refresh with `findNfts` |
|
|
494
|
+
| `socket-unreachable` | Socket couldn't open | On Node 20, pass `webSocket` (below) |
|
|
495
|
+
| `socket-closed` | Socket was disposed | Connect a new one |
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Setup notes
|
|
500
|
+
|
|
501
|
+
### WebAssembly engine
|
|
502
|
+
|
|
503
|
+
The engine (~2 MB) loads on first use. To control when it loads (for example, to show a
|
|
504
|
+
spinner), call it at startup:
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
await initGeeq();
|
|
508
|
+
isGeeqReady(); // true
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
Functions ending in `Sync` need the engine loaded first. Phrases, hashing, hex and base64
|
|
512
|
+
don't need it at all.
|
|
513
|
+
|
|
514
|
+
**Vite** needs one line of config:
|
|
515
|
+
|
|
516
|
+
```ts
|
|
517
|
+
export default defineConfig({ optimizeDeps: { exclude: ["@geeqdev/geeq-sdk"] } });
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
Webpack 5, Rollup and Parcel work without config. To load the engine from your own URL:
|
|
521
|
+
`initGeeq({ wasm: "/assets/geeq_core_bg.wasm" })`.
|
|
522
|
+
|
|
523
|
+
### Node 20
|
|
524
|
+
|
|
525
|
+
Node 20 has no built-in WebSocket. Pass one in (Node 22+ and browsers don't need this):
|
|
526
|
+
|
|
527
|
+
```ts
|
|
528
|
+
import WebSocket from "ws";
|
|
529
|
+
GeeqSocket.connect({ endpoint, webSocket: (url) => new WebSocket(url) });
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
### Testing without a node
|
|
533
|
+
|
|
534
|
+
Pass a fake `transport` (anything shaped like `fetch`):
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
const chain = await GeeqChain.connect({
|
|
538
|
+
endpoint: "https://10.0.0.1:2082",
|
|
539
|
+
penniesPerGeeq,
|
|
540
|
+
transport: async () => ({ ok: true, status: 200, text: async () => JSON.stringify({ result: ledger }) }),
|
|
541
|
+
});
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
You can also pass a fake `webSocket` to `GeeqSocket.connect`. Call `GeeqChain.reset()` in
|
|
545
|
+
`afterEach` so the default chain doesn't leak between tests.
|
|
546
|
+
|
|
547
|
+
`transport` is also how you route requests through your own proxy if CORS blocks you.
|
|
548
|
+
|
|
549
|
+
### Signing offline
|
|
550
|
+
|
|
551
|
+
To sign without a network (for tests, replays or air-gapped machines), give the chain
|
|
552
|
+
values yourself:
|
|
553
|
+
|
|
554
|
+
```ts
|
|
555
|
+
const tx = await createAttestationTx({
|
|
556
|
+
keypair, attestHash, chainNumber: 1, blockNumber: 42, nodeNumber: 3, nonce: 7,
|
|
557
|
+
});
|
|
558
|
+
// or: createAttestationTx({ keypair, attestHash, chainState })
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
The output is byte-for-byte the same as the online version. Other offline builders:
|
|
562
|
+
`createCoinTransferTx`, `createNftMintProxyTx`, `createNftMintTx`. Offline builders skip all
|
|
563
|
+
balance and ownership checks.
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## Reference
|
|
568
|
+
|
|
569
|
+
All exports. Names marked `(Sync)` also have a synchronous version that needs a loaded engine.
|
|
570
|
+
|
|
571
|
+
| Area | Exports |
|
|
572
|
+
|---|---|
|
|
573
|
+
| Engine | `initGeeq`, `isGeeqReady` |
|
|
574
|
+
| Phrases | `generateMnemonic`, `validateMnemonic`, `normalizeMnemonic`, `MNEMONIC_WORD_COUNTS` |
|
|
575
|
+
| Keypairs | `generateKeypair(Sync)`, `importKeypair(Sync)`, `keypairFromSecretKey(Sync)`, `keypairFromMnemonic(Sync)`, `verifySignature`, `accountIdFromPublicKey`, `ACCOUNT_ID_BYTES`, `PUBLIC_KEY_BYTES` |
|
|
576
|
+
| Addresses | `createWalletAddress(Sync)`, `decodeWalletAddress(Sync)`, `isValidWalletAddress`, `GEEQ_RECORD_TYPES`, `DEFAULT_RECORD_TYPE` |
|
|
577
|
+
| Chains and nodes | `GeeqChain`, `GeeqNode`, `initChain`, `attest`, `transfer`, `selectNode`, `GEEQ_NODE_PORT`, `discover`, `listChains`, `listNodes`, `defaultTransport` |
|
|
578
|
+
| Transactions (offline) | `createAttestationTx(Sync)`, `createCoinTransferTx(Sync)`, `createNftMintProxyTx(Sync)`, `createNftMintTx(Sync)` |
|
|
579
|
+
| Amounts | `toPennies`, `formatGeeq` |
|
|
580
|
+
| Content and encoding | `attestHashOf`, `attestHashOfSync`, `readContent`, `assertAttestHash`, `ATTEST_HASH_BYTES`, `inspectMetadata`, `toHex`, `fromHex`, `toBase64`, `fromBase64` |
|
|
581
|
+
| Sockets | `GeeqSocket`, `GeeqTrackedTx`, `transactionIdOf`, `defaultWebSocketFactory` |
|
|
582
|
+
| Block explorer | `getExplorerTransaction` |
|
|
583
|
+
| Node routes | `ledgerRoute`, `submitRoute`, `socketRoute`, `attestationSubmitBody`, `coinTransferSubmitBody` |
|
|
584
|
+
| Ledgers | `decodeValidationLedger`, `GeeqLedger` |
|
|
585
|
+
| Errors | `GeeqError`, `GeeqErrorCode` |
|
|
586
|
+
|
|
587
|
+
TypeScript types are exported next to their functions (`GeeqKeypair`, `AttestationTx`,
|
|
588
|
+
`GeeqChainState`, `GeeqFee`, `GeeqTxOutcome`, and others).
|
|
589
|
+
|
|
590
|
+
### Not supported yet
|
|
591
|
+
|
|
592
|
+
- **Sending transactions.** Your app does the `fetch`.
|
|
593
|
+
- **Getting a result after a page reload.** Save `transactionIdOf(tx)` and use the explorer.
|
|
594
|
+
- **Tokens and other asset types.** Only coin, attestations and NFTs for now.
|
package/package.json
CHANGED
|
@@ -1,6 +1,72 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geeqdev/geeq-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Geeq blockchain SDK for wallets, transfers and NFTs. Works in the browser and Node.js.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"geeq",
|
|
7
|
+
"blockchain",
|
|
8
|
+
"sdk",
|
|
9
|
+
"wallet",
|
|
10
|
+
"transactions",
|
|
11
|
+
"nft",
|
|
12
|
+
"typescript",
|
|
13
|
+
"browser"
|
|
14
|
+
],
|
|
15
|
+
"license": "UNLICENSED",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"main": "./dist/node/index.cjs",
|
|
22
|
+
"module": "./dist/browser/index.js",
|
|
23
|
+
"browser": "./dist/browser/index.js",
|
|
24
|
+
"types": "./dist/types/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"browser": {
|
|
28
|
+
"types": "./dist/types/index.d.ts",
|
|
29
|
+
"default": "./dist/browser/index.js"
|
|
30
|
+
},
|
|
31
|
+
"import": {
|
|
32
|
+
"types": "./dist/types/index.d.ts",
|
|
33
|
+
"default": "./dist/node/index.js"
|
|
34
|
+
},
|
|
35
|
+
"require": {
|
|
36
|
+
"types": "./dist/types/index.d.cts",
|
|
37
|
+
"default": "./dist/node/index.cjs"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"./geeq_core_bg.wasm": "./dist/browser/geeq_core_bg.wasm",
|
|
41
|
+
"./package.json": "./package.json"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"README.md"
|
|
46
|
+
],
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"registry": "https://registry.npmjs.org/",
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build-example": "tsup && node scripts/copy-wasm.mjs",
|
|
53
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
54
|
+
"test": "jest",
|
|
55
|
+
"smoke": "node scripts/smoke.mjs",
|
|
56
|
+
"build": "npm run build"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@noble/hashes": "^1.8.0",
|
|
60
|
+
"@scure/bip39": "^1.6.0",
|
|
61
|
+
"capnp-ts": "^0.4.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@jest/globals": "^30.5.2",
|
|
65
|
+
"@types/jest": "^30.0.0",
|
|
66
|
+
"@types/node": "^22.10.0",
|
|
67
|
+
"jest": "^30.5.2",
|
|
68
|
+
"ts-jest": "^29.2.5",
|
|
69
|
+
"tsup": "^8.3.5",
|
|
70
|
+
"typescript": "^5.6.3"
|
|
71
|
+
}
|
|
72
|
+
}
|